aboutsummaryrefslogtreecommitdiff
path: root/lib/classes
diff options
context:
space:
mode:
authorPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
committerPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
commit4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch)
tree5c07151ae61276d334e88f6309c30d439a85c12e /lib/classes
parentda0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff)
parent97a188592c679890a25c37ab78463add76a52ff7 (diff)
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'lib/classes')
-rw-r--r--lib/classes/ActionMenu.php2
-rw-r--r--lib/classes/AdminCourseFilter.php (renamed from lib/classes/AdminCourseFilter.class.php)47
-rw-r--r--lib/classes/Assets.php (renamed from lib/classes/Assets.class.php)2
-rw-r--r--lib/classes/AuthenticatedController.php32
-rw-r--r--lib/classes/AuthorObject.php (renamed from lib/classes/AuthorObject.class.php)5
-rw-r--r--lib/classes/AutoInsert.php (renamed from lib/classes/AutoInsert.class.php)2
-rw-r--r--lib/classes/AuxLockRules.class.php124
-rw-r--r--lib/classes/Avatar.php (renamed from lib/classes/Avatar.class.php)0
-rw-r--r--lib/classes/BreadCrumb.php (renamed from lib/classes/BreadCrumb.class.php)0
-rw-r--r--lib/classes/Button.php (renamed from lib/classes/Button.class.php)0
-rw-r--r--lib/classes/CSVArrayObject.php (renamed from lib/classes/CSVArrayObject.class.php)0
-rw-r--r--lib/classes/Color.php (renamed from lib/classes/Color.class.php)4
-rw-r--r--lib/classes/Config.php (renamed from lib/classes/Config.class.php)48
-rw-r--r--lib/classes/CourseAvatar.php (renamed from lib/classes/CourseAvatar.class.php)0
-rw-r--r--lib/classes/CourseConfig.php (renamed from lib/classes/CourseConfig.class.php)2
-rw-r--r--lib/classes/CronJob.php (renamed from lib/classes/CronJob.class.php)2
-rw-r--r--lib/classes/CronjobScheduler.php (renamed from lib/classes/CronjobScheduler.class.php)87
-rw-r--r--lib/classes/DBManager.php (renamed from lib/classes/DBManager.class.php)0
-rw-r--r--lib/classes/DIContainer.php18
-rw-r--r--lib/classes/DataFieldBoolEntry.php (renamed from lib/classes/DataFieldBoolEntry.class.php)0
-rw-r--r--lib/classes/DataFieldComboEntry.php (renamed from lib/classes/DataFieldComboEntry.class.php)0
-rw-r--r--lib/classes/DataFieldDateEntry.php (renamed from lib/classes/DataFieldDateEntry.class.php)0
-rw-r--r--lib/classes/DataFieldEmailEntry.php (renamed from lib/classes/DataFieldEmailEntry.class.php)0
-rw-r--r--lib/classes/DataFieldEntry.php (renamed from lib/classes/DataFieldEntry.class.php)2
-rw-r--r--lib/classes/DataFieldLinkEntry.php (renamed from lib/classes/DataFieldLinkEntry.class.php)0
-rw-r--r--lib/classes/DataFieldPhoneEntry.php (renamed from lib/classes/DataFieldPhoneEntry.class.php)0
-rw-r--r--lib/classes/DataFieldRadioEntry.php (renamed from lib/classes/DataFieldRadioEntry.class.php)0
-rw-r--r--lib/classes/DataFieldSelectboxEntry.php (renamed from lib/classes/DataFieldSelectboxEntry.class.php)0
-rw-r--r--lib/classes/DataFieldSelectboxMultipleEntry.php (renamed from lib/classes/DataFieldSelectboxMultipleEntry.class.php)0
-rw-r--r--lib/classes/DataFieldTextareaEntry.php (renamed from lib/classes/DataFieldTextareaEntry.class.php)0
-rw-r--r--lib/classes/DataFieldTextareai18nEntry.php (renamed from lib/classes/DataFieldTextareai18nEntry.class.php)0
-rw-r--r--lib/classes/DataFieldTextlineEntry.php (renamed from lib/classes/DataFieldTextlineEntry.class.php)0
-rw-r--r--lib/classes/DataFieldTextlinei18nEntry.php (renamed from lib/classes/DataFieldTextlinei18nEntry.class.php)0
-rw-r--r--lib/classes/DataFieldTextmarkupEntry.php (renamed from lib/classes/DataFieldTextmarkupEntry.class.php)0
-rw-r--r--lib/classes/DataFieldTextmarkupi18nEntry.php (renamed from lib/classes/DataFieldTextmarkupi18nEntry.class.php)0
-rw-r--r--lib/classes/DataFieldTimeEntry.php (renamed from lib/classes/DataFieldTimeEntry.class.php)0
-rw-r--r--lib/classes/DatabaseObject.php (renamed from lib/classes/DatabaseObject.class.php)4
-rw-r--r--lib/classes/DateFormatter.php (renamed from lib/classes/DateFormatter.class.php)2
-rw-r--r--lib/classes/DbSnapshot.php (renamed from lib/classes/DbSnapshot.class.php)2
-rw-r--r--lib/classes/DbView.php (renamed from lib/classes/DbView.class.php)2
-rw-r--r--lib/classes/Debug/DebugBar.php12
-rw-r--r--lib/classes/Debug/TraceableStudipPDO.php24
-rw-r--r--lib/classes/Debug/TrailsCollector.php69
-rw-r--r--lib/classes/Event.php (renamed from lib/classes/Event.interface.php)0
-rw-r--r--lib/classes/EventLog.php2
-rw-r--r--lib/classes/Feedback.php (renamed from lib/classes/Feedback.class.php)0
-rw-r--r--lib/classes/FeedbackRange.php (renamed from lib/classes/FeedbackRange.interface.php)0
-rw-r--r--lib/classes/FileLock.php (renamed from lib/classes/FileLock.class.php)0
-rw-r--r--lib/classes/ForumActivity.php2
-rw-r--r--lib/classes/ForumEntry.php4
-rw-r--r--lib/classes/ForumHelpers.php10
-rw-r--r--lib/classes/Fullcalendar.php (renamed from lib/classes/Fullcalendar.class.php)2
-rw-r--r--lib/classes/I18N.php4
-rw-r--r--lib/classes/I18NString.php9
-rw-r--r--lib/classes/Icon.php (renamed from lib/classes/Icon.class.php)11
-rw-r--r--lib/classes/InstituteAvatar.php (renamed from lib/classes/InstituteAvatar.class.php)0
-rw-r--r--lib/classes/InstituteCalendarHelper.php (renamed from lib/classes/InstituteCalendarHelper.class.php)42
-rw-r--r--lib/classes/InstituteConfig.php (renamed from lib/classes/InstituteConfig.class.php)2
-rw-r--r--lib/classes/Interactable.php (renamed from lib/classes/Interactable.class.php)0
-rw-r--r--lib/classes/JSONArrayObject.php (renamed from lib/classes/JSONArrayObject.class.php)0
-rw-r--r--lib/classes/JsonApi/JsonApiController.php6
-rw-r--r--lib/classes/JsonApi/Middlewares/Auth/OAuth1Strategy.php114
-rw-r--r--lib/classes/JsonApi/Middlewares/Authentication.php44
-rw-r--r--lib/classes/JsonApi/Middlewares/StudipMockNavigation.php27
-rw-r--r--lib/classes/JsonApi/NonJsonApiController.php4
-rw-r--r--lib/classes/JsonApi/RouteMap.php27
-rw-r--r--lib/classes/JsonApi/Routes/Clipboards/Authority.php28
-rw-r--r--lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsCreate.php106
-rw-r--r--lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsDelete.php54
-rw-r--r--lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsShow.php28
-rw-r--r--lib/classes/JsonApi/Routes/Clipboards/ClipboardsCreate.php46
-rw-r--r--lib/classes/JsonApi/Routes/Clipboards/ClipboardsDelete.php31
-rw-r--r--lib/classes/JsonApi/Routes/Clipboards/ClipboardsUpdate.php50
-rw-r--r--lib/classes/JsonApi/Routes/Consultations/SlotCreationCount.php105
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php14
-rw-r--r--lib/classes/JsonApi/Routes/Files/RangeFileRefsIndex.php2
-rw-r--r--lib/classes/JsonApi/Routes/Files/SubfilerefsIndex.php11
-rw-r--r--lib/classes/JsonApi/Routes/Files/SubfoldersIndex.php23
-rw-r--r--lib/classes/JsonApi/SchemaMap.php4
-rw-r--r--lib/classes/JsonApi/Schemas/Clipboard.php81
-rw-r--r--lib/classes/JsonApi/Schemas/ClipboardItem.php61
-rw-r--r--lib/classes/JsonApi/Schemas/File.php2
-rw-r--r--lib/classes/JsonApi/Schemas/Folder.php12
-rw-r--r--lib/classes/JsonApi/Schemas/WikiPage.php2
-rw-r--r--lib/classes/LayoutMessage.php (renamed from lib/classes/LayoutMessage.interface.php)0
-rw-r--r--lib/classes/LinkButton.php (renamed from lib/classes/LinkButton.class.php)0
-rw-r--r--lib/classes/LockRules.php (renamed from lib/classes/LockRules.class.php)4
-rw-r--r--lib/classes/Loggable.php (renamed from lib/classes/Loggable.class.php)0
-rw-r--r--lib/classes/LtiLink.php16
-rw-r--r--lib/classes/MVV.php (renamed from lib/classes/MVV.class.php)4
-rw-r--r--lib/classes/Markup.php (renamed from lib/classes/Markup.class.php)5
-rw-r--r--lib/classes/MessageBox.php (renamed from lib/classes/MessageBox.class.php)21
-rw-r--r--lib/classes/Metrics.php2
-rw-r--r--lib/classes/ModulesNotification.php (renamed from lib/classes/ModulesNotification.class.php)28
-rw-r--r--lib/classes/MultiDimArrayObject.php (renamed from lib/classes/MultiDimArrayObject.class.php)13
-rw-r--r--lib/classes/MultiPersonSearch.php (renamed from lib/classes/MultiPersonSearch.class.php)8
-rw-r--r--lib/classes/MvvPerm.php4
-rw-r--r--lib/classes/MvvQuickSearch.php2
-rw-r--r--lib/classes/MyRealmModel.php69
-rw-r--r--lib/classes/NotificationCenter.php (renamed from lib/classes/NotificationCenter.class.php)2
-rw-r--r--lib/classes/OAuth1.php167
-rw-r--r--lib/classes/OAuth2/NegotiatesWithPsr7.php6
-rw-r--r--lib/classes/OpenGraph.php81
-rw-r--r--lib/classes/PageLayout.php51
-rw-r--r--lib/classes/PluginAdministration.php23
-rw-r--r--lib/classes/PluginController.php72
-rw-r--r--lib/classes/Privacy.php3
-rw-r--r--lib/classes/PrivacyObject.php (renamed from lib/classes/PrivacyObject.interface.php)0
-rw-r--r--lib/classes/ProfileModel.php213
-rw-r--r--lib/classes/QuestionType.php (renamed from lib/classes/QuestionType.interface.php)7
-rw-r--r--lib/classes/QuickSearch.php (renamed from lib/classes/QuickSearch.class.php)6
-rw-r--r--lib/classes/Range.php (renamed from lib/classes/Range.interface.php)0
-rw-r--r--lib/classes/RangeConfig.php (renamed from lib/classes/RangeConfig.class.php)2
-rw-r--r--lib/classes/RangeTreeObject.php (renamed from lib/classes/RangeTreeObject.class.php)2
-rw-r--r--lib/classes/RangeTreeObjectFak.php (renamed from lib/classes/RangeTreeObjectFak.class.php)2
-rw-r--r--lib/classes/RangeTreeObjectInst.php (renamed from lib/classes/RangeTreeObjectInst.class.php)2
-rw-r--r--lib/classes/Request.php (renamed from lib/classes/Request.class.php)42
-rw-r--r--lib/classes/ResetButton.php (renamed from lib/classes/ResetButton.class.php)0
-rw-r--r--lib/classes/ResponsiveHelper.php117
-rw-r--r--lib/classes/SQLQuery.php43
-rw-r--r--lib/classes/Score.php (renamed from lib/classes/Score.class.php)6
-rw-r--r--lib/classes/SemBrowse.php (renamed from lib/classes/SemBrowse.class.php)182
-rw-r--r--lib/classes/SemClass.php (renamed from lib/classes/SemClass.class.php)44
-rw-r--r--lib/classes/SemType.php (renamed from lib/classes/SemType.class.php)37
-rw-r--r--lib/classes/Seminar.php (renamed from lib/classes/Seminar.class.php)10
-rw-r--r--lib/classes/SeminarCategories.php (renamed from lib/classes/SeminarCategories.class.php)4
-rw-r--r--lib/classes/SessionDecoder.php (renamed from lib/classes/SessionDecoder.class.php)65
-rw-r--r--lib/classes/SimpleCollection.php782
-rw-r--r--lib/classes/SimpleORMap.php2483
-rw-r--r--lib/classes/SimpleORMapCollection.php258
-rw-r--r--lib/classes/Siteinfo.php20
-rw-r--r--lib/classes/StudipArrayObject.php (renamed from lib/classes/StudipArrayObject.class.php)70
-rw-r--r--lib/classes/StudipAutoloader.php2
-rw-r--r--lib/classes/StudipCache.class.php94
-rw-r--r--lib/classes/StudipCachedArray.php27
-rw-r--r--lib/classes/StudipController.php885
-rw-r--r--lib/classes/StudipControllerPropertiesTrait.php69
-rw-r--r--lib/classes/StudipCoreFormat.php5
-rw-r--r--lib/classes/StudipDbCache.class.php123
-rw-r--r--lib/classes/StudipDispatcher.php64
-rw-r--r--lib/classes/StudipFileloader.php6
-rw-r--r--lib/classes/StudipForm.php (renamed from lib/classes/StudipForm.class.php)2
-rw-r--r--lib/classes/StudipItem.php (renamed from lib/classes/StudipItem.interface.php)0
-rw-r--r--lib/classes/StudipKing.php (renamed from lib/classes/StudipKing.class.php)4
-rw-r--r--lib/classes/StudipLink.php (renamed from lib/classes/StudipLink.class.php)0
-rw-r--r--lib/classes/StudipLock.php (renamed from lib/classes/StudipLock.class.php)2
-rw-r--r--lib/classes/StudipLog.php (renamed from lib/classes/StudipLog.class.php)0
-rw-r--r--lib/classes/StudipLvgruppeSelection.php (renamed from lib/classes/StudipLvgruppeSelection.class.php)0
-rw-r--r--lib/classes/StudipMail.php (renamed from lib/classes/StudipMail.class.php)35
-rw-r--r--lib/classes/StudipMemoryCache.class.php84
-rw-r--r--lib/classes/StudipObject.php (renamed from lib/classes/StudipObject.class.php)4
-rw-r--r--lib/classes/StudipPDO.php (renamed from lib/classes/StudipPDO.class.php)56
-rw-r--r--lib/classes/StudipPDOStatement.php5
-rw-r--r--lib/classes/StudipRangeTree.php (renamed from lib/classes/StudipRangeTree.class.php)2
-rw-r--r--lib/classes/StudipRangeTreeView.php (renamed from lib/classes/StudipRangeTreeView.class.php)2
-rw-r--r--lib/classes/StudipRangeTreeViewAdmin.php (renamed from lib/classes/StudipRangeTreeViewAdmin.class.php)2
-rw-r--r--lib/classes/StudipResponse.php55
-rw-r--r--lib/classes/StudipSemRangeTreeViewSimple.php (renamed from lib/classes/StudipSemRangeTreeViewSimple.class.php)2
-rw-r--r--lib/classes/StudipSemSearch.php (renamed from lib/classes/StudipSemSearch.class.php)2
-rw-r--r--lib/classes/StudipSemSearchHelper.php (renamed from lib/classes/StudipSemSearchHelper.class.php)2
-rw-r--r--lib/classes/StudipSemTree.php (renamed from lib/classes/StudipSemTree.class.php)0
-rw-r--r--lib/classes/StudipSemTreeSearch.php (renamed from lib/classes/StudipSemTreeSearch.class.php)2
-rw-r--r--lib/classes/StudipSemTreeView.php (renamed from lib/classes/StudipSemTreeView.class.php)2
-rw-r--r--lib/classes/StudipSemTreeViewAdmin.php (renamed from lib/classes/StudipSemTreeViewAdmin.class.php)2
-rw-r--r--lib/classes/StudipSemTreeViewSimple.php (renamed from lib/classes/StudipSemTreeViewSimple.class.php)2
-rw-r--r--lib/classes/StudipStudyAreaSelection.php (renamed from lib/classes/StudipStudyAreaSelection.class.php)0
-rw-r--r--lib/classes/StudipTransformFormat.php99
-rw-r--r--lib/classes/StudipTreeNodeCachableTrait.php2
-rw-r--r--lib/classes/StudygroupAvatar.php (renamed from lib/classes/StudygroupAvatar.class.php)0
-rw-r--r--lib/classes/StudygroupModel.php4
-rw-r--r--lib/classes/TreeAbstract.php (renamed from lib/classes/TreeAbstract.class.php)2
-rw-r--r--lib/classes/TreeView.php (renamed from lib/classes/TreeView.class.php)8
-rw-r--r--lib/classes/TwilloConnector.php8
-rw-r--r--lib/classes/UpdateInformation.php (renamed from lib/classes/UpdateInformation.class.php)0
-rw-r--r--lib/classes/UserConfig.php (renamed from lib/classes/UserConfig.class.php)2
-rw-r--r--lib/classes/UserDataAdapter.php30
-rw-r--r--lib/classes/UserLookup.php (renamed from lib/classes/UserLookup.class.php)4
-rw-r--r--lib/classes/UserManagement.php (renamed from lib/classes/UserManagement.class.php)20
-rw-r--r--lib/classes/Visibility.php2
-rw-r--r--lib/classes/WidgetHelper.php402
-rw-r--r--lib/classes/admission/AdmissionAlgorithm.php (renamed from lib/classes/admission/AdmissionAlgorithm.class.php)4
-rw-r--r--lib/classes/admission/AdmissionPriority.php (renamed from lib/classes/admission/AdmissionPriority.class.php)2
-rw-r--r--lib/classes/admission/AdmissionRule.php (renamed from lib/classes/admission/AdmissionRule.class.php)2
-rw-r--r--lib/classes/admission/AdmissionUserList.php (renamed from lib/classes/admission/AdmissionUserList.class.php)2
-rw-r--r--lib/classes/admission/CourseSet.php (renamed from lib/classes/admission/CourseSet.class.php)2
-rw-r--r--lib/classes/admission/RandomAlgorithm.php (renamed from lib/classes/admission/RandomAlgorithm.class.php)2
-rw-r--r--lib/classes/admission/UserFilter.php (renamed from lib/classes/admission/UserFilter.class.php)2
-rw-r--r--lib/classes/admission/UserFilterField.php (renamed from lib/classes/admission/UserFilterField.class.php)7
-rw-r--r--lib/classes/admission/userfilter/DatafieldCondition.php (renamed from lib/classes/admission/userfilter/DatafieldCondition.class.php)2
-rw-r--r--lib/classes/admission/userfilter/DegreeCondition.php (renamed from lib/classes/admission/userfilter/DegreeCondition.class.php)2
-rw-r--r--lib/classes/admission/userfilter/PermissionCondition.php (renamed from lib/classes/admission/userfilter/PermissionCondition.class.php)2
-rw-r--r--lib/classes/admission/userfilter/SemesterOfStudyCondition.php (renamed from lib/classes/admission/userfilter/SemesterOfStudyCondition.class.php)2
-rw-r--r--lib/classes/admission/userfilter/StgteilVersionCondition.php (renamed from lib/classes/admission/userfilter/StgteilVersionCondition.class.php)2
-rw-r--r--lib/classes/admission/userfilter/SubjectCondition.php (renamed from lib/classes/admission/userfilter/SubjectCondition.class.php)2
-rw-r--r--lib/classes/admission/userfilter/SubjectConditionAny.php (renamed from lib/classes/admission/userfilter/SubjectConditionAny.class.php)4
-rw-r--r--lib/classes/assets/SASSCompiler.php4
-rw-r--r--lib/classes/auth_plugins/StudipAuthAbstract.php (renamed from lib/classes/auth_plugins/StudipAuthAbstract.class.php)2
-rw-r--r--lib/classes/auth_plugins/StudipAuthCAS.php (renamed from lib/classes/auth_plugins/StudipAuthCAS.class.php)0
-rw-r--r--lib/classes/auth_plugins/StudipAuthIP.php (renamed from lib/classes/auth_plugins/StudipAuthIP.class.php)2
-rw-r--r--lib/classes/auth_plugins/StudipAuthLTI.php (renamed from lib/classes/auth_plugins/StudipAuthLTI.class.php)23
-rw-r--r--lib/classes/auth_plugins/StudipAuthLdap.php (renamed from lib/classes/auth_plugins/StudipAuthLdap.class.php)2
-rw-r--r--lib/classes/auth_plugins/StudipAuthLdapReadAndBind.php (renamed from lib/classes/auth_plugins/StudipAuthLdapReadAndBind.class.php)2
-rw-r--r--lib/classes/auth_plugins/StudipAuthOIDC.php (renamed from lib/classes/auth_plugins/StudipAuthOIDC.class.php)2
-rw-r--r--lib/classes/auth_plugins/StudipAuthSSO.php (renamed from lib/classes/auth_plugins/StudipAuthSSO.class.php)2
-rw-r--r--lib/classes/auth_plugins/StudipAuthShib.php (renamed from lib/classes/auth_plugins/StudipAuthShib.class.php)2
-rw-r--r--lib/classes/auth_plugins/StudipAuthStandard.php (renamed from lib/classes/auth_plugins/StudipAuthStandard.class.php)2
-rw-r--r--lib/classes/cache/Cache.php213
-rw-r--r--lib/classes/cache/DbCache.php143
-rw-r--r--lib/classes/cache/Exception.php27
-rw-r--r--lib/classes/cache/Factory.php (renamed from lib/classes/StudipCacheFactory.class.php)53
-rw-r--r--lib/classes/cache/FileCache.php (renamed from lib/classes/StudipFileCache.class.php)192
-rw-r--r--lib/classes/cache/InvalidCacheArgumentException.php28
-rw-r--r--lib/classes/cache/Item.php164
-rw-r--r--lib/classes/cache/KeyTrait.php (renamed from lib/classes/StudipCacheKeyTrait.php)10
-rw-r--r--lib/classes/cache/MemcachedCache.php (renamed from lib/classes/StudipMemcachedCache.php)110
-rw-r--r--lib/classes/cache/MemoryCache.php98
-rw-r--r--lib/classes/cache/Proxy.php (renamed from lib/classes/StudipCacheProxy.php)98
-rw-r--r--lib/classes/cache/RedisCache.php (renamed from lib/classes/StudipRedisCache.class.php)95
-rw-r--r--lib/classes/cache/Wrapper.php (renamed from lib/classes/StudipCacheWrapper.php)76
-rw-r--r--lib/classes/calendar/CalendarScheduleModel.php2
-rw-r--r--lib/classes/calendar/EventData.php (renamed from lib/classes/calendar/EventData.class.php)0
-rw-r--r--lib/classes/calendar/EventSource.php (renamed from lib/classes/calendar/EventSource.interface.php)0
-rw-r--r--lib/classes/calendar/ICalendarExport.php (renamed from lib/classes/calendar/ICalendarExport.class.php)100
-rw-r--r--lib/classes/calendar/ICalendarImport.php (renamed from lib/classes/calendar/ICalendarImport.class.php)0
-rw-r--r--lib/classes/calendar/Owner.php (renamed from lib/classes/calendar/Owner.interface.php)0
-rw-r--r--lib/classes/cas/CAS_PGTStorage_Cache.php4
-rw-r--r--lib/classes/coursewizardsteps/AdvancedBasicDataWizardStep.php2
-rw-r--r--lib/classes/coursewizardsteps/BasicDataWizardStep.php11
-rw-r--r--lib/classes/coursewizardsteps/LVGroupsWizardStep.php35
-rw-r--r--lib/classes/coursewizardsteps/StudyAreasWizardStep.php4
-rw-r--r--lib/classes/exportdocument/ExportDocument.php (renamed from lib/classes/exportdocument/ExportDocument.interface.php)0
-rw-r--r--lib/classes/exportdocument/ExportPDF.php (renamed from lib/classes/exportdocument/ExportPDF.class.php)7
-rw-r--r--lib/classes/forms/Captcha.php29
-rw-r--r--lib/classes/forms/CaptchaInput.php38
-rw-r--r--lib/classes/forms/Form.php12
-rw-r--r--lib/classes/forms/Input.php11
-rw-r--r--lib/classes/forms/Part.php2
-rw-r--r--lib/classes/globalsearch/GlobalSearchCourses.php4
-rw-r--r--lib/classes/globalsearch/GlobalSearchCourseware.php2
-rw-r--r--lib/classes/globalsearch/GlobalSearchMessages.php6
-rw-r--r--lib/classes/globalsearch/GlobalSearchMyCourses.php4
-rw-r--r--lib/classes/globalsearch/GlobalSearchUsers.php2
-rw-r--r--lib/classes/helpbar/Helpbar.php5
-rw-r--r--lib/classes/librarysearch/LibraryDocument.php (renamed from lib/classes/librarysearch/LibraryDocument.class.php)9
-rw-r--r--lib/classes/librarysearch/LibraryResultParser.php (renamed from lib/classes/librarysearch/LibraryResultParser.interface.php)0
-rw-r--r--lib/classes/librarysearch/LibrarySearch.php (renamed from lib/classes/librarysearch/LibrarySearch.class.php)0
-rw-r--r--lib/classes/librarysearch/LibrarySearchManager.php (renamed from lib/classes/librarysearch/LibrarySearchManager.class.php)0
-rw-r--r--lib/classes/librarysearch/resultparsers/BASELibraryResultParser.php (renamed from lib/classes/librarysearch/resultparsers/BASELibraryResultParser.class.php)0
-rw-r--r--lib/classes/librarysearch/resultparsers/K10PlusLibraryResultParser.php (renamed from lib/classes/librarysearch/resultparsers/K10PlusLibraryResultParser.class.php)0
-rw-r--r--lib/classes/librarysearch/resultparsers/MarcxmlLibraryResultParser.php (renamed from lib/classes/librarysearch/resultparsers/MarcxmlLibraryResultParser.class.php)0
-rw-r--r--lib/classes/librarysearch/resultparsers/SRULibraryResultParser.php (renamed from lib/classes/librarysearch/resultparsers/SRULibraryResultParser.class.php)0
-rw-r--r--lib/classes/librarysearch/searchmodules/BASELibrarySearch.php (renamed from lib/classes/librarysearch/searchmodules/BASELibrarySearch.class.php)0
-rw-r--r--lib/classes/librarysearch/searchmodules/K10PlusZentralLibrarySearch.php (renamed from lib/classes/librarysearch/searchmodules/K10PlusZentralLibrarySearch.class.php)0
-rw-r--r--lib/classes/librarysearch/searchmodules/SRULibrarySearch.php (renamed from lib/classes/librarysearch/searchmodules/SRULibrarySearch.class.php)0
-rw-r--r--lib/classes/restapi/ConsumerPermissions.php212
-rw-r--r--lib/classes/restapi/Response.php164
-rw-r--r--lib/classes/restapi/RouteMap.php1060
-rw-r--r--lib/classes/restapi/Router.php665
-rw-r--r--lib/classes/restapi/RouterException.php31
-rw-r--r--lib/classes/restapi/RouterHalt.php19
-rw-r--r--lib/classes/restapi/UriTemplate.php115
-rw-r--r--lib/classes/restapi/UserPermissions.php144
-rw-r--r--lib/classes/restapi/consumer/Base.php226
-rw-r--r--lib/classes/restapi/consumer/HTTP.php50
-rw-r--r--lib/classes/restapi/consumer/OAuth.php231
-rw-r--r--lib/classes/restapi/consumer/Studip.php36
-rw-r--r--lib/classes/restapi/renderer/DebugRenderer.php57
-rw-r--r--lib/classes/restapi/renderer/DefaultRenderer.php74
-rw-r--r--lib/classes/restapi/renderer/JSONRenderer.php35
-rw-r--r--lib/classes/searchtypes/MyCoursesSearch.php (renamed from lib/classes/searchtypes/MyCoursesSearch.class.php)2
-rw-r--r--lib/classes/searchtypes/NewsRangesSearch.php2
-rw-r--r--lib/classes/searchtypes/PermissionSearch.php (renamed from lib/classes/searchtypes/PermissionSearch.class.php)2
-rw-r--r--lib/classes/searchtypes/RangeSearch.php (renamed from lib/classes/searchtypes/RangeSearch.class.php)0
-rw-r--r--lib/classes/searchtypes/ResourceSearch.php (renamed from lib/classes/searchtypes/ResourceSearch.class.php)2
-rw-r--r--lib/classes/searchtypes/RoomSearch.php (renamed from lib/classes/searchtypes/RoomSearch.class.php)2
-rw-r--r--lib/classes/searchtypes/SQLSearch.php (renamed from lib/classes/searchtypes/SQLSearch.class.php)4
-rw-r--r--lib/classes/searchtypes/SearchType.php (renamed from lib/classes/searchtypes/SearchType.class.php)3
-rw-r--r--lib/classes/searchtypes/SeminarSearch.php (renamed from lib/classes/searchtypes/SeminarSearch.class.php)2
-rw-r--r--lib/classes/searchtypes/StandardSearch.php (renamed from lib/classes/searchtypes/StandardSearch.class.php)4
-rw-r--r--lib/classes/searchtypes/TreeSearch.php (renamed from lib/classes/searchtypes/TreeSearch.class.php)2
-rw-r--r--lib/classes/sidebar/AttributesArrayAccessTrait.php26
-rw-r--r--lib/classes/sidebar/ClipboardWidget.php (renamed from lib/classes/sidebar/ClipboardWidget.class.php)0
-rw-r--r--lib/classes/sidebar/InstituteSelectWidget.php (renamed from lib/classes/sidebar/InstituteSelectWidget.class.php)0
-rw-r--r--lib/classes/sidebar/LinksWidget.php2
-rw-r--r--lib/classes/sidebar/ResourceTreeWidget.php (renamed from lib/classes/sidebar/ResourceTreeWidget.class.php)0
-rw-r--r--lib/classes/sidebar/RoomClipboardWidget.php (renamed from lib/classes/sidebar/RoomClipboardWidget.class.php)0
-rw-r--r--lib/classes/sidebar/RoomSearchTreeWidget.php (renamed from lib/classes/sidebar/RoomSearchTreeWidget.class.php)0
-rw-r--r--lib/classes/sidebar/RoomSearchWidget.php (renamed from lib/classes/sidebar/RoomSearchWidget.class.php)0
-rw-r--r--lib/classes/sidebar/Sidebar.php4
-rw-r--r--lib/classes/sidebar/TemplateWidget.php4
290 files changed, 7587 insertions, 5836 deletions
diff --git a/lib/classes/ActionMenu.php b/lib/classes/ActionMenu.php
index 0822f72..25351ce 100644
--- a/lib/classes/ActionMenu.php
+++ b/lib/classes/ActionMenu.php
@@ -410,7 +410,7 @@ class ActionMenu
$rendering_mode = $this->rendering_mode;
if ($rendering_mode === null) {
- $rendering_mode = $this->countActions() <= Config::get()->ACTION_MENU_THRESHOLD
+ $rendering_mode = $this->countActions() <= 1 // Config::get()->ACTION_MENU_THRESHOLD
? self::RENDERING_MODE_ICONS
: self::RENDERING_MODE_MENU;
}
diff --git a/lib/classes/AdminCourseFilter.class.php b/lib/classes/AdminCourseFilter.php
index da8ce54..f643e09 100644
--- a/lib/classes/AdminCourseFilter.class.php
+++ b/lib/classes/AdminCourseFilter.php
@@ -28,6 +28,8 @@
class AdminCourseFilter
{
static protected $instance = null;
+
+ /** @var SQLQuery|null */
public $query = null;
public $max_show_courses = 500;
public $settings = [];
@@ -84,21 +86,11 @@ class AdminCourseFilter
return $a['Institut_id'];
});
} else {
- //We must check, if the institute ID belongs to a faculty
- //and has the string _i appended to it.
- //In that case we must display the courses of the faculty
- //and all its institutes.
- //Otherwise we just display the courses of the faculty.
- $include_children = false;
$inst_id = $GLOBALS['user']->cfg->MY_INSTITUTES_DEFAULT;
- if (str_contains($inst_id, '_')) {
- $inst_id = substr($inst_id, 0, strpos($inst_id, '_'));
- $include_children = true;
- }
$inst_ids[] = $inst_id;
- if ($include_children) {
+ if ($GLOBALS['user']->cfg->MY_INSTITUTES_INCLUDE_CHILDREN) {
$inst = Institute::find($inst_id);
if ($inst && $inst->isFaculty()) {
foreach ($inst->sub_institutes->pluck('Institut_id') as $institut_id) {
@@ -109,13 +101,10 @@ class AdminCourseFilter
}
if (Config::get()->ALLOW_ADMIN_RELATED_INST) {
- $sem_inst = 'seminar_inst';
- $this->query->join('seminar_inst', 'seminar_inst', 'seminar_inst.seminar_id = seminare.Seminar_id');
+ $this->query->where('seminar_inst', 'EXISTS (SELECT 1 FROM seminar_inst WHERE seminar_id = seminare.Seminar_id AND institut_id IN (:institut_ids))');
} else {
- $sem_inst = 'seminare';
+ $this->query->where("seminar_inst", "seminare.institut_id IN (:institut_ids)");
}
-
- $this->query->where('seminar_inst', "$sem_inst.institut_id IN (:institut_ids)");
$this->query->parameter('institut_ids', $inst_ids);
if ($GLOBALS['user']->cfg->MY_COURSES_SELECTED_CYCLE) {
@@ -196,6 +185,16 @@ class AdminCourseFilter
}
/**
+ * @param int|null $limit
+ * @return Course[]
+ */
+ public function fetchCourses(?int $limit = null): array
+ {
+ NotificationCenter::postNotification('AdminCourseFilterWillQuery', $this);
+ return $this->query->fetchAll(Course::class, $limit);
+ }
+
+ /**
* Returns the data of the resultset of the AdminCourseFilter.
*
* @param string $order_by possible values name or number
@@ -206,16 +205,16 @@ class AdminCourseFilter
*/
public function getCoursesForAdminWidget(string $order_by = 'name')
{
- $count_courses = $this->countCourses();
- $order = 'seminare.name';
- if ($order_by === 'number') {
- $order = 'seminare.veranstaltungsnummer, seminare.name';
- }
- if ($count_courses && $count_courses <= $this->max_show_courses) {
+ try {
+ $order = 'seminare.name';
+ if ($order_by === 'number') {
+ $order = 'seminare.veranstaltungsnummer, seminare.name';
+ }
$this->query->orderBy($order);
- return $this->getCourses();
+ return $this->fetchCourses($this->max_show_courses);
+ } catch (OverflowException $e) {
+ return [];
}
- return [];
}
}
diff --git a/lib/classes/Assets.class.php b/lib/classes/Assets.php
index 928c47c..d4d792e 100644
--- a/lib/classes/Assets.class.php
+++ b/lib/classes/Assets.php
@@ -6,7 +6,7 @@
# Lifter010: TODO
/*
- * Assets.class.php - assets helper
+ * Assets.php - assets helper
*
* Copyright (C) 2007 - Marcus Lunzenauer <mlunzena@uos.de>
*
diff --git a/lib/classes/AuthenticatedController.php b/lib/classes/AuthenticatedController.php
new file mode 100644
index 0000000..e051ffa
--- /dev/null
+++ b/lib/classes/AuthenticatedController.php
@@ -0,0 +1,32 @@
+<?php
+/*
+ * Copyright (C) 2009 - Marcus Lunzenauer <mlunzena@uos.de>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ */
+
+class AuthenticatedController extends StudipController
+{
+ protected $with_session = true; //we do need to have a session for this controller
+ protected $allow_nobody = false; //nobody is not allowed and always gets a login-screen
+
+ public function before_filter(&$action, &$args)
+ {
+ parent::before_filter($action, $args);
+
+ // Restore request if present
+ if (isset($this->flash['request'])) {
+ foreach ($this->flash['request'] as $key => $value) {
+ Request::set($key, $value);
+ }
+ }
+ }
+
+ protected function keepRequest()
+ {
+ $this->flash['request'] = Request::getInstance()->getIterator()->getArrayCopy();
+ }
+}
diff --git a/lib/classes/AuthorObject.class.php b/lib/classes/AuthorObject.php
index b3e98e3..f08622e 100644
--- a/lib/classes/AuthorObject.class.php
+++ b/lib/classes/AuthorObject.php
@@ -5,7 +5,7 @@
# Lifter010: TODO
// +--------------------------------------------------------------------------+
// This file is part of Stud.IP
-// AuthorObject.class.php
+// AuthorObject.php
//
// Class to provide basic properties of an object in Stud.IP
//
@@ -31,7 +31,7 @@ define("ERROR_CRITICAL", "8");
/**
- * AuthorObject.class.php
+ * AuthorObject.php
*
* Class to provide basic properties of an object in Stud.IP
*
@@ -136,4 +136,3 @@ class AuthorObject
$class->resetErrors();
}
}
-
diff --git a/lib/classes/AutoInsert.class.php b/lib/classes/AutoInsert.php
index b2168cc..b5cc005 100644
--- a/lib/classes/AutoInsert.class.php
+++ b/lib/classes/AutoInsert.php
@@ -14,7 +14,7 @@
*/
/**
- * AutoInsert.class.php
+ * AutoInsert.php
* Provides functions required by StEP00216:
* - Assign seminars for automatic registration of certain user types
* - Maintenance of registration rules
diff --git a/lib/classes/AuxLockRules.class.php b/lib/classes/AuxLockRules.class.php
deleted file mode 100644
index 4d06ca2..0000000
--- a/lib/classes/AuxLockRules.class.php
+++ /dev/null
@@ -1,124 +0,0 @@
-<?
-# Lifter002: DONE
-# Lifter007: TODO
-# Lifter003: TEST
-# Lifter010: TODO
-/**
-* ZusatzLockRules.class.php - Sichtbarkeits-Administration fuer Zusatzangaben bei Teilnehmerlisten
-*
-* Copyright (C) 2006 Till Glöggler <tgloeggl@inspace.de>
-*
-* This program is free software; you can redistribute it and/or
-* modify it under the terms of the GNU General Public License
-* as published by the Free Software Foundation; either version 2
-* of the License, or (at your option) any later version.
-*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
-*
-* You should have received a copy of the GNU General Public License
-* along with this program; if not, write to the Free Software
-* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-*/
-
-/**
- * @deprecated since Stud.IP 5.3
- */
-class AuxLockRules
-{
-
- static function _toArray($data)
- {
- return [
- 'lock_id' => $data['lock_id'],
- 'name' => $data['name'],
- 'description' => $data['description'],
- 'attributes' => json_decode($data['attributes'], true),
- 'order' => json_decode($data['sorting'], true)
- ];
- }
-
- static function getAllLockRules()
- {
- $ret = [];
- $db = DBManager::get()->query("SELECT * FROM aux_lock_rules");
- while ($data = $db->fetch(PDO::FETCH_ASSOC)) {
- $ret[$data['lock_id']] = AuxLockRules::_toArray($data);
- }
-
- return $ret;
- }
-
- static function getLockRuleById($id)
- {
- $stmt = DBManager::get()->prepare("SELECT * FROM aux_lock_rules WHERE lock_id = ?");
- $stmt->execute([$id]);
- $data = $stmt->fetch(PDO::FETCH_ASSOC);
- return AuxLockRules::_toArray($data);
- }
-
- static function getLockRuleBySemId($sem_id)
- {
- $stmt = DBManager::get()->prepare("SELECT aux_lock_rule FROM seminare WHERE Seminar_id = ?");
- $stmt->execute([$sem_id]);
- $lock_rule = $stmt->fetchColumn();
- if ($lock_rule) {
- return AuxLockRules::getLockRuleById($lock_rule);
- }
- return NULL;
- }
-
- static function createLockRule($name, $description, $fields, $order)
- {
- $id = md5(uniqid(rand()));
- $attributes = json_encode($fields);
- $sorting = json_encode($order);
- $stmt = DBManager::get()->prepare('INSERT INTO aux_lock_rules '
- . '(lock_id, name, description, attributes, sorting) '
- . 'VALUES (?, ?, ?, ?, ?)');
- $stmt->execute([$id, $name, $description, $attributes, $sorting]);
- return $id;
- }
-
- static function updateLockRule($id, $name, $description, $fields, $order)
- {
- $attributes = json_encode($fields);
- $sorting = json_encode($order);
- $stmt = DBManager::get()->prepare('UPDATE aux_lock_rules '
- . 'SET name = ?, description = ?, attributes = ?, sorting = ? '
- . 'WHERE lock_id = ?');
- return $stmt->execute([$name, $description, $attributes, $sorting, $id]);
- }
-
- static function deleteLockRule($id)
- {
- $stmt = DBManager::get()->prepare('SELECT COUNT(*) as c FROM seminare WHERE aux_lock_rule = ?');
- $stmt->execute([$id]);
- if ($stmt->fetchColumn() > 0) return false;
-
- $stmt = DBManager::get()->prepare('DELETE FROM aux_lock_rules WHERE lock_id = ?');
- return $stmt->execute([$id]);
- }
-
- static function getSemFields()
- {
- return [
- 'vasemester' => 'Semester',
- 'vanr' => 'Veranstaltungsnummer',
- 'vatitle' => 'Veranstaltungstitel',
- 'vadozent' => 'Dozent'
- ];
- }
-
- static function checkLockRule($fields)
- {
- $entries = DataField::getDataFields('usersemdata');
- foreach ($entries as $id => $entry) {
- if ($fields[$entry->datafield_id] == 1) return true;
- }
-
- return false;
- }
-}
diff --git a/lib/classes/Avatar.class.php b/lib/classes/Avatar.php
index 959523f..959523f 100644
--- a/lib/classes/Avatar.class.php
+++ b/lib/classes/Avatar.php
diff --git a/lib/classes/BreadCrumb.class.php b/lib/classes/BreadCrumb.php
index 73a2086..73a2086 100644
--- a/lib/classes/BreadCrumb.class.php
+++ b/lib/classes/BreadCrumb.php
diff --git a/lib/classes/Button.class.php b/lib/classes/Button.php
index 1c2c437..1c2c437 100644
--- a/lib/classes/Button.class.php
+++ b/lib/classes/Button.php
diff --git a/lib/classes/CSVArrayObject.class.php b/lib/classes/CSVArrayObject.php
index f075795..f075795 100644
--- a/lib/classes/CSVArrayObject.class.php
+++ b/lib/classes/CSVArrayObject.php
diff --git a/lib/classes/Color.class.php b/lib/classes/Color.php
index a9b4506..dcd68f5 100644
--- a/lib/classes/Color.class.php
+++ b/lib/classes/Color.php
@@ -1,6 +1,6 @@
<?php
/**
- * lib/classes/Color.class.php - class to mix colors and convert them between different types
+ * lib/classes/Color.php - class to mix colors and convert them between different types
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
@@ -367,4 +367,4 @@ class Color {
return [$H, $S, $L];
}
-} \ No newline at end of file
+}
diff --git a/lib/classes/Config.class.php b/lib/classes/Config.php
index 4d57f0f..c49a845 100644
--- a/lib/classes/Config.class.php
+++ b/lib/classes/Config.php
@@ -1,6 +1,6 @@
<?php
/**
- * Config.class.php
+ * Config.php
* provides access to global configuration
*
* This program is free software; you can redistribute it and/or
@@ -127,9 +127,14 @@ class Config implements ArrayAccess, Countable, IteratorAggregate
*/
public function getValue($field)
{
+ if ($this->fromEnv($field)) {
+ return $_ENV["STUDIP_CONFIG_{$field}"];
+ }
+
if (array_key_exists($field, $this->data)) {
return $this->data[$field];
}
+
if (isset($GLOBALS[$field]) && !isset($_REQUEST[$field])) {
return $GLOBALS[$field];
}
@@ -153,11 +158,8 @@ class Config implements ArrayAccess, Countable, IteratorAggregate
/**
* IteratorAggregate
- *
- * @todo Add Traversable return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function getIterator()
+ public function getIterator(): Traversable
{
return new ArrayIterator($this->data);
}
@@ -188,55 +190,40 @@ class Config implements ArrayAccess, Countable, IteratorAggregate
/**
* ArrayAccess: Check whether the given offset exists.
- *
- * @todo Add bool return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetExists($offset)
+ public function offsetExists($offset): bool
{
return isset($this->$offset);
}
/**
* ArrayAccess: Get the value at the given offset.
- *
- * @todo Add mixed return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetGet($offset)
+ public function offsetGet($offset): mixed
{
return $this->$offset;
}
/**
* ArrayAccess: Set the value at the given offset.
- *
- * @todo Add void return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetSet($offset, $value)
+ public function offsetSet($offset, $value): void
{
$this->$offset = $value;
}
/**
* ArrayAccess: unset the value at the given offset (not applicable)
- *
- * @todo Add void return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetUnset($offset)
+ public function offsetUnset($offset): void
{
}
/**
* Countable
- *
- * @todo Add void return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function count()
+ public function count(): int
{
return count($this->data);
}
@@ -468,4 +455,15 @@ class Config implements ArrayAccess, Countable, IteratorAggregate
return (string) $value;
}
+
+ /**
+ * Returns whether the value was set from the environment.
+ *
+ * @param string $field
+ * @return bool
+ */
+ public function fromEnv(string $field): bool
+ {
+ return isset($_ENV["STUDIP_CONFIG_{$field}"]);
+ }
}
diff --git a/lib/classes/CourseAvatar.class.php b/lib/classes/CourseAvatar.php
index 8c153a8..8c153a8 100644
--- a/lib/classes/CourseAvatar.class.php
+++ b/lib/classes/CourseAvatar.php
diff --git a/lib/classes/CourseConfig.class.php b/lib/classes/CourseConfig.php
index ad62be0..3ac5587 100644
--- a/lib/classes/CourseConfig.class.php
+++ b/lib/classes/CourseConfig.php
@@ -1,6 +1,6 @@
<?php
/**
- * CourseConfig.class.php
+ * CourseConfig.php
* provides access to course preferences
*
* This program is free software; you can redistribute it and/or
diff --git a/lib/classes/CronJob.class.php b/lib/classes/CronJob.php
index 1e7fb41..dee65e7 100644
--- a/lib/classes/CronJob.class.php
+++ b/lib/classes/CronJob.php
@@ -10,7 +10,7 @@
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// Cronjob.class.php
+// Cronjob.php
//
// Copyright (C) 2013 Jan-Hendrik Willms <tleilax+studip@gmail.com>
// +---------------------------------------------------------------------------+
diff --git a/lib/classes/CronjobScheduler.class.php b/lib/classes/CronjobScheduler.php
index 67db94c..fbb5b66 100644
--- a/lib/classes/CronjobScheduler.class.php
+++ b/lib/classes/CronjobScheduler.php
@@ -10,7 +10,7 @@
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// CronjobScheduler.class.php
+// CronjobScheduler.php
//
// Copyright (C) 2013 Jan-Hendrik Willms <tleilax+studip@gmail.com>
// +---------------------------------------------------------------------------+
@@ -132,35 +132,6 @@ class CronjobScheduler
}
/**
- * Schedules a task for a single execution at the provided time.
- *
- * @param String $task_id The id of the task to be executed
- * @param int $timestamp When the task should be executed
- * @param String $priority Priority of the execution (low, normal, high),
- * defaults to normal
- * @param Array $parameters Optional parameters passed to the task
- * @return CronjobSchedule The generated schedule object.
- */
- public function scheduleOnce($task_id, $timestamp, $priority = CronjobSchedule::PRIORITY_NORMAL,
- $parameters = [])
- {
- $schedule = new CronjobSchedule();
- $schedule->type = 'once';
- $schedule->task_id = $task_id;
- $schedule->parameters = $parameters;
- $schedule->priority = $priority;
- $schedule->next_execution = $timestamp;
-
- $schedule->store();
-
- $task = $schedule->task;
- $task->assigned_count += 1;
- $task->store();
-
- return $schedule;
- }
-
- /**
* Schedules a task for periodic execution with the provided schedule.
*
* @param String $task_id The id of the task to be executed
@@ -185,21 +156,21 @@ class CronjobScheduler
* - 1 >= x >= 7 for "exactly at day of week x"
* (x starts with monday at 1 and ends with
* sunday at 7)
- * @param String $priority Priority of the execution (low, normal, high),
- * defaults to normal
* @param Array $parameters Optional parameters passed to the task
* @return CronjobSchedule The generated schedule object.
*/
- public function schedulePeriodic($task_id, $minute = null, $hour = null,
- $day = null, $month = null, $day_of_week = null,
- $priority = CronjobSchedule::PRIORITY_NORMAL,
- $parameters = [])
- {
+ public function schedule(
+ string $task_id,
+ ?int $minute = null,
+ ?int $hour = null,
+ ?int $day = null,
+ ?int $month = null,
+ ?int $day_of_week = null,
+ array $parameters = []
+ ): CronjobSchedule {
$schedule = new CronjobSchedule();
- $schedule->type = 'periodic';
$schedule->task_id = $task_id;
$schedule->parameters = $parameters;
- $schedule->priority = $priority;
$schedule->minute = $minute;
$schedule->hour = $hour;
@@ -217,6 +188,24 @@ class CronjobScheduler
}
/**
+ * An alias for schedule for backwards compatibility.
+ *
+ * @see CronjobScheduler::schedule()
+ */
+ public function schedulePeriodic(
+ $task_id,
+ $minute = null,
+ $hour = null,
+ $day = null,
+ $month = null,
+ $day_of_week = null,
+ $priority = null,
+ $parameters = []
+ ) {
+ return $this->schedule($task_id, $minute, $hour, $day, $month, $day_of_week, $parameters);
+ }
+
+ /**
* Cancels the provided schedule.
*
* @param String $schedule_id Id of the schedule to be canceled
@@ -258,8 +247,8 @@ class CronjobScheduler
}
// Find all schedules that are due to execute and which task is active
- $temp = CronjobSchedule::findBySQL('active = 1 AND next_execution <= UNIX_TIMESTAMP() '
- .'ORDER BY priority DESC, next_execution ASC');
+ $temp = CronjobSchedule::findBySQL('`active` = 1 AND `next_execution` <= UNIX_TIMESTAMP() '
+ .'ORDER BY `next_execution`');
$schedules = array_filter($temp, function ($schedule) { return $schedule->task->active; });
if (count($schedules) === 0) {
@@ -267,20 +256,19 @@ class CronjobScheduler
}
foreach ($schedules as $schedule) {
+ $log = new CronjobLog();
+ $log->schedule_id = $schedule->schedule_id;
+ $log->scheduled = $schedule->next_execution;
+ $log->executed = time();
+ $log->exception = null;
+ $log->duration = -1;
+
try {
// Skip schedules with missing task classes
if (!$schedule->task->valid) {
throw new Exception(_('Die Klasse für den Cronjob-Task konnte nicht gefunden werden'));
}
- $log = new CronjobLog();
- $log->schedule_id = $schedule->schedule_id;
- $log->scheduled = $schedule->next_execution;
- $log->executed = time();
- $log->exception = null;
- $log->duration = -1;
- $log->store();
-
// Start capturing output and measuring duration
ob_start();
$start_time = microtime(true);
@@ -297,6 +285,7 @@ class CronjobScheduler
$log->store();
} catch (Exception $e) {
$log->exception = $e;
+ $log->store();
// Deactivate schedule
$schedule->deactivate();
diff --git a/lib/classes/DBManager.class.php b/lib/classes/DBManager.php
index 7854cff..7854cff 100644
--- a/lib/classes/DBManager.class.php
+++ b/lib/classes/DBManager.php
diff --git a/lib/classes/DIContainer.php b/lib/classes/DIContainer.php
index 9f0721b..c3d9b82 100644
--- a/lib/classes/DIContainer.php
+++ b/lib/classes/DIContainer.php
@@ -17,7 +17,7 @@ class DIContainer
/**
* Get the globally available instance of the container.
*
- * @return static
+ * @return ContainerInterface
*/
public static function getInstance()
{
@@ -47,9 +47,11 @@ class DIContainer
{
$builder = new ContainerBuilder();
if (\Studip\ENV == 'production') {
- $builder->enableCompilation($GLOBALS['TMP_PATH']);
+ $builder->enableCompilation(
+ self::getCompilationPath(),
+ self::getCompilationClass()
+ );
}
- $builder->ignorePhpDocErrors(true);
$builder->addDefinitions('lib/bootstrap-definitions.php');
$jsonapiSettings = require 'lib/classes/JsonApi/settings.php';
@@ -60,4 +62,14 @@ class DIContainer
return $builder;
}
+
+ public static function getCompilationPath(): string
+ {
+ return $GLOBALS['TMP_PATH'];
+ }
+
+ public static function getCompilationClass(): string
+ {
+ return 'CompiledContainer';
+ }
}
diff --git a/lib/classes/DataFieldBoolEntry.class.php b/lib/classes/DataFieldBoolEntry.php
index da294b6..da294b6 100644
--- a/lib/classes/DataFieldBoolEntry.class.php
+++ b/lib/classes/DataFieldBoolEntry.php
diff --git a/lib/classes/DataFieldComboEntry.class.php b/lib/classes/DataFieldComboEntry.php
index 6047050..6047050 100644
--- a/lib/classes/DataFieldComboEntry.class.php
+++ b/lib/classes/DataFieldComboEntry.php
diff --git a/lib/classes/DataFieldDateEntry.class.php b/lib/classes/DataFieldDateEntry.php
index bf7d518..bf7d518 100644
--- a/lib/classes/DataFieldDateEntry.class.php
+++ b/lib/classes/DataFieldDateEntry.php
diff --git a/lib/classes/DataFieldEmailEntry.class.php b/lib/classes/DataFieldEmailEntry.php
index aafe7bb..aafe7bb 100644
--- a/lib/classes/DataFieldEmailEntry.class.php
+++ b/lib/classes/DataFieldEmailEntry.php
diff --git a/lib/classes/DataFieldEntry.class.php b/lib/classes/DataFieldEntry.php
index fd50a0b..eca0b4a 100644
--- a/lib/classes/DataFieldEntry.class.php
+++ b/lib/classes/DataFieldEntry.php
@@ -1,6 +1,6 @@
<?php
/**
- * DataFieldEntry.class.php
+ * DataFieldEntry.php
*
* @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
* @author Marcus Lunzenauer <mlunzena@uos.de>
diff --git a/lib/classes/DataFieldLinkEntry.class.php b/lib/classes/DataFieldLinkEntry.php
index 702907f..702907f 100644
--- a/lib/classes/DataFieldLinkEntry.class.php
+++ b/lib/classes/DataFieldLinkEntry.php
diff --git a/lib/classes/DataFieldPhoneEntry.class.php b/lib/classes/DataFieldPhoneEntry.php
index d3a0e70..d3a0e70 100644
--- a/lib/classes/DataFieldPhoneEntry.class.php
+++ b/lib/classes/DataFieldPhoneEntry.php
diff --git a/lib/classes/DataFieldRadioEntry.class.php b/lib/classes/DataFieldRadioEntry.php
index 8699b69..8699b69 100644
--- a/lib/classes/DataFieldRadioEntry.class.php
+++ b/lib/classes/DataFieldRadioEntry.php
diff --git a/lib/classes/DataFieldSelectboxEntry.class.php b/lib/classes/DataFieldSelectboxEntry.php
index 8ac1a3c..8ac1a3c 100644
--- a/lib/classes/DataFieldSelectboxEntry.class.php
+++ b/lib/classes/DataFieldSelectboxEntry.php
diff --git a/lib/classes/DataFieldSelectboxMultipleEntry.class.php b/lib/classes/DataFieldSelectboxMultipleEntry.php
index 3abb373..3abb373 100644
--- a/lib/classes/DataFieldSelectboxMultipleEntry.class.php
+++ b/lib/classes/DataFieldSelectboxMultipleEntry.php
diff --git a/lib/classes/DataFieldTextareaEntry.class.php b/lib/classes/DataFieldTextareaEntry.php
index a48be98..a48be98 100644
--- a/lib/classes/DataFieldTextareaEntry.class.php
+++ b/lib/classes/DataFieldTextareaEntry.php
diff --git a/lib/classes/DataFieldTextareai18nEntry.class.php b/lib/classes/DataFieldTextareai18nEntry.php
index 9b6db40..9b6db40 100644
--- a/lib/classes/DataFieldTextareai18nEntry.class.php
+++ b/lib/classes/DataFieldTextareai18nEntry.php
diff --git a/lib/classes/DataFieldTextlineEntry.class.php b/lib/classes/DataFieldTextlineEntry.php
index 6062ed1..6062ed1 100644
--- a/lib/classes/DataFieldTextlineEntry.class.php
+++ b/lib/classes/DataFieldTextlineEntry.php
diff --git a/lib/classes/DataFieldTextlinei18nEntry.class.php b/lib/classes/DataFieldTextlinei18nEntry.php
index 9d1f344..9d1f344 100644
--- a/lib/classes/DataFieldTextlinei18nEntry.class.php
+++ b/lib/classes/DataFieldTextlinei18nEntry.php
diff --git a/lib/classes/DataFieldTextmarkupEntry.class.php b/lib/classes/DataFieldTextmarkupEntry.php
index 50115b9..50115b9 100644
--- a/lib/classes/DataFieldTextmarkupEntry.class.php
+++ b/lib/classes/DataFieldTextmarkupEntry.php
diff --git a/lib/classes/DataFieldTextmarkupi18nEntry.class.php b/lib/classes/DataFieldTextmarkupi18nEntry.php
index 0b3d7e6..0b3d7e6 100644
--- a/lib/classes/DataFieldTextmarkupi18nEntry.class.php
+++ b/lib/classes/DataFieldTextmarkupi18nEntry.php
diff --git a/lib/classes/DataFieldTimeEntry.class.php b/lib/classes/DataFieldTimeEntry.php
index 96e5835..96e5835 100644
--- a/lib/classes/DataFieldTimeEntry.class.php
+++ b/lib/classes/DataFieldTimeEntry.php
diff --git a/lib/classes/DatabaseObject.class.php b/lib/classes/DatabaseObject.php
index 2258adb..5c54229 100644
--- a/lib/classes/DatabaseObject.class.php
+++ b/lib/classes/DatabaseObject.php
@@ -5,7 +5,7 @@
# Lifter010: TODO
// +--------------------------------------------------------------------------+
// This file is part of Stud.IP
-// DatabaseObject.class.php
+// DatabaseObject.php
//
// Class to provide basic properties of an DatabseObject in Stud.IP
//
@@ -35,7 +35,7 @@ define("INSTANCEOF_DATABASEOBJECT", "DatabaseObject");
/**
- * DatabaseObject.class.php
+ * DatabaseObject.php
*
* Class to provide basic properties of an DatabaseObject in Stud.IP
*
diff --git a/lib/classes/DateFormatter.class.php b/lib/classes/DateFormatter.php
index 5f7c558..5309cbc 100644
--- a/lib/classes/DateFormatter.class.php
+++ b/lib/classes/DateFormatter.php
@@ -1,7 +1,7 @@
<?php
# Lifter010: TODO
/**
- * DateFormater.class.php - Handles the formatting of one date and associated rooms.
+ * DateFormater.php - Handles the formatting of one date and associated rooms.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
diff --git a/lib/classes/DbSnapshot.class.php b/lib/classes/DbSnapshot.php
index dfb4f89..33ea660 100644
--- a/lib/classes/DbSnapshot.class.php
+++ b/lib/classes/DbSnapshot.php
@@ -5,7 +5,7 @@
# Lifter010: TODO
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// DbSnapshot.class.php
+// DbSnapshot.php
// Class to provide snapshots of mysql result sets
// Uses PHPLib DB Abstraction
// Copyright (c) 2002 André Noack <andre.noack@gmx.net>
diff --git a/lib/classes/DbView.class.php b/lib/classes/DbView.php
index 81e9b91..0215f8e 100644
--- a/lib/classes/DbView.class.php
+++ b/lib/classes/DbView.php
@@ -5,7 +5,7 @@
# Lifter010: TODO
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// DbView.class.php
+// DbView.php
// Class to provide simple Views and Prepared Statements
// Mainly for MySql, may work with other DBs (not tested)
// Copyright (c) 2002 André Noack <andre.noack@gmx.net>
diff --git a/lib/classes/Debug/DebugBar.php b/lib/classes/Debug/DebugBar.php
new file mode 100644
index 0000000..bbd80c9
--- /dev/null
+++ b/lib/classes/Debug/DebugBar.php
@@ -0,0 +1,12 @@
+<?php
+namespace Studip\Debug;
+
+final class DebugBar
+{
+ public static function isActivated(): bool
+ {
+ return \Studip\ENV === 'development'
+ && ($_ENV['DEBUG_BAR'] ?? false)
+ && class_exists(\DebugBar\DebugBar::class);
+ }
+}
diff --git a/lib/classes/Debug/TraceableStudipPDO.php b/lib/classes/Debug/TraceableStudipPDO.php
new file mode 100644
index 0000000..6100c22
--- /dev/null
+++ b/lib/classes/Debug/TraceableStudipPDO.php
@@ -0,0 +1,24 @@
+<?php
+namespace Studip\Debug;
+
+use DebugBar\DataCollector\PDO\TraceablePDO;
+
+final class TraceableStudipPDO extends TraceablePDO
+{
+ /**
+ * Quotes a string for use in a query.
+ *
+ * @link http://php.net/manual/en/pdo.quote.php
+ * @param string $string The string to be quoted.
+ * @param int $parameter_type [optional] Provides a data type hint for drivers that have
+ * alternate quoting styles.
+ * @return string|bool A quoted string that is theoretically safe to pass into an SQL statement.
+ * Returns FALSE if the driver does not support quoting in this way.
+ */
+ #[\ReturnTypeWillChange]
+ public function quote($string, $parameter_type = null)
+ {
+ return $this->pdo->quote($string, $parameter_type);
+ }
+
+}
diff --git a/lib/classes/Debug/TrailsCollector.php b/lib/classes/Debug/TrailsCollector.php
new file mode 100644
index 0000000..f4b8a65
--- /dev/null
+++ b/lib/classes/Debug/TrailsCollector.php
@@ -0,0 +1,69 @@
+<?php
+namespace Studip\Debug;
+
+use DebugBar\DataCollector\DataCollector;
+use DebugBar\DataCollector\Renderable;
+use Trails\Controller;
+
+final class TrailsCollector extends DataCollector implements Renderable
+{
+ public function __construct(
+ private readonly Controller $controller
+ ) {
+ $this->useHtmlVarDumper(false);
+ }
+
+ public function collect()
+ {
+ $data = [];
+ foreach ($this->controller->get_assigned_variables() as $k => $v) {
+ if ($this->isHtmlVarDumperUsed()) {
+ $v = $this->getVarDumper()->renderVar($v);
+ } else if (!is_string($v)) {
+ $v = $this->getDataFormatter()->formatVar($v);
+ }
+ $data[$k] = $v;
+ }
+
+ ksort($data);
+
+ return $data;
+ }
+
+ public function getName()
+ {
+ return 'trails';
+ }
+
+ /**
+ * @return array
+ */
+ public function getAssets()
+ {
+ return $this->isHtmlVarDumperUsed() ? $this->getVarDumper()->getAssets() : [];
+ }
+
+ /**
+ * @return array[]
+ */
+ public function getWidgets()
+ {
+ $name = $this->getName();
+ $widget = $this->isHtmlVarDumperUsed()
+ ? 'PhpDebugBar.Widgets.HtmlVariableListWidget'
+ : 'PhpDebugBar.Widgets.VariableListWidget';
+
+ return [
+ $name => [
+ 'icon' => 'code',
+ 'widget' => $widget,
+ 'map' => $name,
+ 'default' => '{}'
+ ],
+ "{$name}:badge" => [
+ 'map' => "{$name}:variable__count",
+ 'default' => count($this->controller->get_assigned_variables()),
+ ],
+ ];
+ }
+}
diff --git a/lib/classes/Event.interface.php b/lib/classes/Event.php
index 23f092b..23f092b 100644
--- a/lib/classes/Event.interface.php
+++ b/lib/classes/Event.php
diff --git a/lib/classes/EventLog.php b/lib/classes/EventLog.php
index 7336415..b20b997 100644
--- a/lib/classes/EventLog.php
+++ b/lib/classes/EventLog.php
@@ -87,7 +87,7 @@ class EventLog
$offset = (int) $offset;
$filter = $this->sql_event_filter($action_id, $object_id, $parameters) ?: '1';
- $filter .= " ORDER BY mkdate DESC, event_id DESC LIMIT {$offset}, 50";
+ $filter .= " ORDER BY event_id DESC LIMIT {$offset}, 50";
$log_events = LogEvent::findBySQL($filter, $parameters);
$events = [];
diff --git a/lib/classes/Feedback.class.php b/lib/classes/Feedback.php
index 26dbf55..26dbf55 100644
--- a/lib/classes/Feedback.class.php
+++ b/lib/classes/Feedback.php
diff --git a/lib/classes/FeedbackRange.interface.php b/lib/classes/FeedbackRange.php
index 863c197..863c197 100644
--- a/lib/classes/FeedbackRange.interface.php
+++ b/lib/classes/FeedbackRange.php
diff --git a/lib/classes/FileLock.class.php b/lib/classes/FileLock.php
index 064d41e..064d41e 100644
--- a/lib/classes/FileLock.class.php
+++ b/lib/classes/FileLock.php
diff --git a/lib/classes/ForumActivity.php b/lib/classes/ForumActivity.php
index bc00cf1..a6a685d 100644
--- a/lib/classes/ForumActivity.php
+++ b/lib/classes/ForumActivity.php
@@ -124,7 +124,7 @@ class ForumActivity
'mkdate' => $post['mkdate'] ?? time()
];
- if ($post['anonymous']) {
+ if (!empty($post['anonymous'])) {
$data['actor_type'] = 'anonymous';
$data['actor_id'] = '';
}
diff --git a/lib/classes/ForumEntry.php b/lib/classes/ForumEntry.php
index f98e59c..7ac5306 100644
--- a/lib/classes/ForumEntry.php
+++ b/lib/classes/ForumEntry.php
@@ -218,7 +218,7 @@ class ForumEntry implements PrivacyObject
array_pop($path);
$data = array_pop($path);
- return $data['id'] ?: false;
+ return $data['id'] ?? false;
}
@@ -1029,7 +1029,7 @@ class ForumEntry implements PrivacyObject
$stmt = DBManager::get()->prepare("SELECT chdate FROM forum_entries
WHERE lft > ? AND rgt < ? AND seminar_id = ?
ORDER BY chdate DESC LIMIT 1");
- $stmt->execute([$parent['lft'], $parent['rgt'], $parent['seminar_id']]);
+ $stmt->execute([$parent['lft'] ?? null, $parent['rgt'] ?? null, $parent['seminar_id'] ?? null]);
$chdate = $stmt->fetchColumn();
$stmt_insert = DBManager::get()->prepare("UPDATE forum_entries
diff --git a/lib/classes/ForumHelpers.php b/lib/classes/ForumHelpers.php
index 3054dbc..0c036cf 100644
--- a/lib/classes/ForumHelpers.php
+++ b/lib/classes/ForumHelpers.php
@@ -92,19 +92,15 @@ class ForumHelpers {
*/
public static function translate_perm($perm)
{
- $mapping = [
+ return match ($perm) {
'root' => _('Root'),
'admin' => _('Administrator/-in'),
'dozent' => _('Lehrende/-r'),
'tutor' => _('Tutor/-in'),
'autor' => _('Autor/-in'),
'user' => _('Leser/-in'),
- ];
-
- // TODO: Activate next when devboard reliably runs on PHP7
- // return $mapping[$perm] ?? '';
-
- return isset($mapping[$perm]) ? $mapping[$perm] : '';
+ default => '',
+ };
}
/**
diff --git a/lib/classes/Fullcalendar.class.php b/lib/classes/Fullcalendar.php
index 467b583..db8364a 100644
--- a/lib/classes/Fullcalendar.class.php
+++ b/lib/classes/Fullcalendar.php
@@ -64,7 +64,7 @@ class Fullcalendar
public function render()
{
- $factory = new \Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'] . '/templates');
+ $factory = new \Flexi\Factory($GLOBALS['STUDIP_BASE_PATH'] . '/templates');
$template = $factory->open('studip-fullcalendar.php');
$real_data_name = sprintf('data-%s', $this->data_name);
return $template->render(
diff --git a/lib/classes/I18N.php b/lib/classes/I18N.php
index d34ed0b..44252f3 100644
--- a/lib/classes/I18N.php
+++ b/lib/classes/I18N.php
@@ -61,10 +61,10 @@ class I18N
/**
* Protected constructor in order to always force a specific input type.
*
- * @param string|Flexi_Template $template Template to use
+ * @param string|Flexi\Template $template Template to use
* @param string $name Name of the element
* @param string|I18NString $value Value of the element
- * @param array $attributes Additional variables for the
+ * @param array $attributes Additional variables for the
* element
*/
final protected function __construct($template, $name, $value, array $attributes)
diff --git a/lib/classes/I18NString.php b/lib/classes/I18NString.php
index 353062e..eab42ff 100644
--- a/lib/classes/I18NString.php
+++ b/lib/classes/I18NString.php
@@ -78,13 +78,8 @@ class I18NString implements JsonSerializable
/**
* Return the JSON representation of this i18n field in selected language.
- *
- * @return string
- *
- * @todo Add mixed return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function jsonSerialize()
+ public function jsonSerialize(): string
{
return (string) $this;
}
@@ -184,7 +179,7 @@ class I18NString implements JsonSerializable
*/
public function translation($lang)
{
- return $this->toArray()[$lang] ?? '';
+ return $this->toArray()[$lang] ?? null;
}
/**
diff --git a/lib/classes/Icon.class.php b/lib/classes/Icon.php
index ca0b9e0..6c586a0 100644
--- a/lib/classes/Icon.class.php
+++ b/lib/classes/Icon.php
@@ -338,7 +338,7 @@ class Icon
$classNames = 'icon-role-' . $this->role;
if (!self::isStatic($this->shape)) {
- $classNames .= ' icon-shape-' . $this->shape;
+ $classNames .= ' icon-shape-' . $this->shapeToPath($this->shape);
}
$result['class'] = isset($result['class']) ? $result['class'] . ' ' . $classNames : $classNames;
@@ -385,8 +385,11 @@ class Icon
// transforms a shape w/ possible additions (`shape`) to a path `(addition/)?shape`
private function shapeToPath()
{
- return self::isStatic($this->shape)
- ? $this->shape :
- join('/', array_reverse(explode('+', preg_replace('/\.svg$/', '', $this->shape))));
+ if (self::isStatic($this->shape)) {
+ return $this->shape;
+ }
+ $shape = array_reverse(explode('/', $this->shape))[0];
+ $shape = explode('+', $shape)[0];
+ return $shape;
}
}
diff --git a/lib/classes/InstituteAvatar.class.php b/lib/classes/InstituteAvatar.php
index 8adbfba..8adbfba 100644
--- a/lib/classes/InstituteAvatar.class.php
+++ b/lib/classes/InstituteAvatar.php
diff --git a/lib/classes/InstituteCalendarHelper.class.php b/lib/classes/InstituteCalendarHelper.php
index 9412a34..6c934b4 100644
--- a/lib/classes/InstituteCalendarHelper.class.php
+++ b/lib/classes/InstituteCalendarHelper.php
@@ -1,7 +1,7 @@
<?php
/**
- * InstituteCalendarHelper.class.php - class for institute calendar convenience functions
+ * InstituteCalendarHelper.php - class for institute calendar convenience functions
*
* @author Timo Hartge <hartge@data-quest>
* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
@@ -43,14 +43,6 @@ class InstituteCalendarHelper
*/
public static function getResourceColumns($institut_id, $only_visible = false)
{
- /*
- returns the columns in following format:
- $columns = [
- ['id' => '0', 'title' => 'Sammelspalte'],
- ['id' => '1', 'title' => 'Spalte 1']
- ];
- */
-
$columns = [];
$inst_columns = [
0 => ['Sammelspalte', 1]
@@ -206,13 +198,13 @@ class InstituteCalendarHelper
/**
* Looks up default color value for institute course events
*
- * @param Course $course
+ * @param SimpleORMap $context
*
* @return array course events with color value
*/
- public static function getInstituteDefaultEventcolors($institut)
+ public static function getInstituteDefaultEventcolors($context)
{
- $df = DatafieldEntryModel::findByModel($institut, self::INST_DEFAULT_COLOR_DATAFIELD_ID);
+ $df = DatafieldEntryModel::findByModel($context, self::INST_DEFAULT_COLOR_DATAFIELD_ID);
if ($df && $df[0]->content) {
$event_colors = unserialize($df[0]->content);
} else {
@@ -224,17 +216,17 @@ class InstituteCalendarHelper
/**
* Sets default institute course events color value for semtypes
*
- * @param Institut $institut
+ * @param SimpleORMap $context
* @param string $semtype
* @param string $color colorcode
*
* @return bool stored
*/
- public static function setInstituteDefaultEventcolor($institut, $semtype, $color)
+ public static function setInstituteDefaultEventcolor($context, $semtype, $color)
{
- $df = DatafieldEntryModel::findByModel($institut, self::INST_DEFAULT_COLOR_DATAFIELD_ID);
+ $df = DatafieldEntryModel::findByModel($context, self::INST_DEFAULT_COLOR_DATAFIELD_ID);
if ($df[0]) {
- $event_colors = self::getInstituteDefaultEventcolors($institut);
+ $event_colors = self::getInstituteDefaultEventcolors($context);
$event_colors[$semtype['name']] = $color;
$df[0]->content = serialize($event_colors);
return $df[0]->store();
@@ -317,7 +309,6 @@ class InstituteCalendarHelper
*/
public static function getEvents($courses, $institut_id, $semester = null, $specific_weekday = null)
{
- $events = [];
$today = date('w');
$user_insts = array_map(function ($arr) {
@@ -330,9 +321,15 @@ class InstituteCalendarHelper
$maxbigtime = (int) $max_time[0];
$institut = Institute::find($institut_id);
+
+ if (!$institut) {
+ return [];
+ }
+
$inst_default_colors = self::getInstituteDefaultEventcolors($institut);
- Course::findAndMapMany(function ($course) use (
+ $events = [];
+ Course::findEachMany(function ($course) use (
$courses,
&$events,
$today,
@@ -418,8 +415,8 @@ class InstituteCalendarHelper
}
if (!$backgroundcolor) {
$backgroundcolor = array_key_exists($semtype['name'], $inst_default_colors)
- ? $inst_default_colors[$semtype['name']]
- : self::DEFAULT_EVENT_COLOR;
+ ? $inst_default_colors[$semtype['name']]
+ : self::DEFAULT_EVENT_COLOR;
}
$textcolor = '#ffffff';
@@ -472,7 +469,7 @@ class InstituteCalendarHelper
* @param SeminarCycleDate $cycle_date
* @param string $institut_id
*
- * @return string enriched course info string for tooltip
+ * @return array enriched course info string for tooltip
*/
public static function getCycleEvent($cycle_date, $institut_id)
{
@@ -594,9 +591,8 @@ class InstituteCalendarHelper
*/
private static function getCycleInfos($course, $cycle_date)
{
- $info_string = '';
- $info_string .= $course->getFullName('number-name') . "\n";
+ $info_string = $course->getFullName('number-name') . "\n";
$dozenten = [];
foreach (CourseMember::findByCourseAndStatus($course->id, 'dozent') as $cmember) {
diff --git a/lib/classes/InstituteConfig.class.php b/lib/classes/InstituteConfig.php
index d1a51cb..551ea4d 100644
--- a/lib/classes/InstituteConfig.class.php
+++ b/lib/classes/InstituteConfig.php
@@ -1,6 +1,6 @@
<?php
/**
- * InstituteConfig.class.php
+ * InstituteConfig.php
* provides access to institute preferences
*
* @author Jan-Hendrik Wullms <tleilax+studip@gmail.com>
diff --git a/lib/classes/Interactable.class.php b/lib/classes/Interactable.php
index 9796f91..9796f91 100644
--- a/lib/classes/Interactable.class.php
+++ b/lib/classes/Interactable.php
diff --git a/lib/classes/JSONArrayObject.class.php b/lib/classes/JSONArrayObject.php
index 896be3e..896be3e 100644
--- a/lib/classes/JSONArrayObject.class.php
+++ b/lib/classes/JSONArrayObject.php
diff --git a/lib/classes/JsonApi/JsonApiController.php b/lib/classes/JsonApi/JsonApiController.php
index 1718c52..614650d 100644
--- a/lib/classes/JsonApi/JsonApiController.php
+++ b/lib/classes/JsonApi/JsonApiController.php
@@ -375,12 +375,12 @@ class JsonApiController
*
* @param Request $request Request der eingehende Request
*
- * @return mixed entweder null oder das User-Objekt des
- * "eingeloggten" Nutzers
+ * @return null|\User entweder null oder das User-Objekt des "eingeloggten"
+ * Nutzers
*/
public function getUser(Request $request)
{
- return $request->getAttribute(Authentication::USER_KEY, null);
+ return $request->getAttribute(Authentication::USER_KEY);
}
/**
diff --git a/lib/classes/JsonApi/Middlewares/Auth/OAuth1Strategy.php b/lib/classes/JsonApi/Middlewares/Auth/OAuth1Strategy.php
deleted file mode 100644
index 113ee09..0000000
--- a/lib/classes/JsonApi/Middlewares/Auth/OAuth1Strategy.php
+++ /dev/null
@@ -1,114 +0,0 @@
-<?php
-
-namespace JsonApi\Middlewares\Auth;
-
-use Psr\Http\Message\ResponseInterface as Response;
-use Psr\Http\Message\ServerRequestInterface as Request;
-
-class OAuth1Strategy implements Strategy
-{
- /** @var callable */
- protected $authenticator;
-
- /** @var Request */
- protected $request;
-
- /** @var ?\User */
- protected $user;
-
- /**
- * @param callable $authenticator
- */
- public function __construct(Request $request, $authenticator)
- {
- $this->request = $request;
- $this->authenticator = $authenticator;
-
- \OAuthStore::instance('PDO', ['conn' => \DBManager::get()]);
- }
-
- public function check()
- {
- return !is_null($this->user());
- }
-
- public function user()
- {
- if (!is_null($this->user)) {
- return $this->user;
- }
-
- $this->user = $this->detect();
-
- return $this->user;
- }
-
- public function addChallenge(Response $response)
- {
- return $response; //->withHeader('WWW-Authenticate', sprintf('Basic realm="%s"', 'Stud.IP JSON-API'));
- }
-
- private function detect(): ?\User
- {
- if (!\OAuthRequestVerifier::requestIsSigned()) {
- return null;
- }
-
- $uri = (string) $this->request->getUri();
- $method = $this->request->getMethod();
-
- if ('GET' === strtoupper(($method))) {
- $parameters = (array) $this->request->getQueryParams();
- } elseif ('POST' === strtoupper(($method))) {
- $parameters = (array) $this->request->getParsedBody();
- } else {
- $parameters = [];
- }
- $parameters = $this->getParamsFromAuthorizationHeader($this->request, $parameters);
-
- $req = new \OAuthRequestVerifier($uri, $method, $parameters);
-
- // Check oauth timestamp and deny access if timestamp is outdated
- if ($req->getParam('oauth_timestamp') < strtotime('-6 hours')) {
- return null;
- }
-
- $result = $req->verifyExtended('access');
-
- $query = 'SELECT user_id FROM api_oauth_user_mapping WHERE oauth_id = ?';
- $statement = \DBManager::get()->prepare($query);
- $statement->execute([$result['user_id']]);
-
- if (!$userId = $statement->fetchColumn()) {
- return null;
- }
-
- /** @var \User */
- return \User::find($userId);
- }
-
- private function getParamsFromAuthorizationHeader(Request $request, array $params): array
- {
- if ($request->hasHeader('Authorization')) {
- $auth = $request->getHeaderLine('Authorization');
- if (0 == strncasecmp($auth, 'OAuth', 4)) {
- foreach (explode(',', substr($auth, 6)) as $v) {
- if (!strpos($v, '=')) {
- continue;
- }
- $v = trim($v);
- list($name, $value) = explode('=', $v, 2);
- if (!empty($value) && '"' == $value[0] && '"' == substr($value, -1)) {
- $value = substr(substr($value, 1), 0, -1);
- }
-
- if (0 != strcasecmp($name, 'realm')) {
- $params[$name] = $value;
- }
- }
- }
- }
-
- return $params;
- }
-}
diff --git a/lib/classes/JsonApi/Middlewares/Authentication.php b/lib/classes/JsonApi/Middlewares/Authentication.php
index de92e15..bbcfef1 100644
--- a/lib/classes/JsonApi/Middlewares/Authentication.php
+++ b/lib/classes/JsonApi/Middlewares/Authentication.php
@@ -15,22 +15,21 @@ class Authentication
// $user = $request->getAttribute(Authentication::USER_KEY);
const USER_KEY = 'studip-user';
- // a callable accepting two arguments username and password and
- // returning either null or a Stud.IP user object
- /** @var callable */
- private $authenticator;
-
/**
* Der Konstruktor.
*
- * @param callable $authenticator ein Callable, das den Nutzernamen und
+ * @param \Closure $authenticator eine Closure, die den Nutzernamen und
* das Passwort als Argumente erhält und
* damit entweder einen Stud.IP-User-Objekt
* oder null zurückgibt
+ * @param array $excluded_strategies
*/
- public function __construct($authenticator)
- {
- $this->authenticator = $authenticator;
+ public function __construct(
+ // a callable accepting two arguments username and password and
+ // returning either null or a Stud.IP user object
+ private readonly \Closure $authenticator,
+ private readonly array $excluded_strategies = []
+ ) {
}
/**
@@ -45,12 +44,7 @@ class Authentication
*/
public function __invoke(Request $request, RequestHandler $handler)
{
- $guards = [
- new Auth\SessionStrategy(),
- new Auth\HttpBasicAuthStrategy($request, $this->authenticator),
- new Auth\OAuth2Strategy($request, $this->authenticator),
- new Auth\OAuth1Strategy($request, $this->authenticator),
- ];
+ $guards = $this->getGuards($request);
foreach ($guards as $guard) {
if ($guard->check()) {
@@ -101,4 +95,24 @@ class Authentication
return $request->withAttribute(self::USER_KEY, $user);
}
+
+ /**
+ * @param Request $request
+ *
+ * @return array
+ */
+ protected function getGuards(Request $request): array
+ {
+ $guards = [
+ 'session' => new Auth\SessionStrategy(),
+ 'basic' => new Auth\HttpBasicAuthStrategy($request, $this->authenticator),
+ 'oauth2' => new Auth\OAuth2Strategy($request, $this->authenticator),
+ ];
+
+ foreach ($this->excluded_strategies as $strategy) {
+ unset($guards[$strategy]);
+ }
+
+ return $guards;
+ }
}
diff --git a/lib/classes/JsonApi/Middlewares/StudipMockNavigation.php b/lib/classes/JsonApi/Middlewares/StudipMockNavigation.php
index 34225d4..0883809 100644
--- a/lib/classes/JsonApi/Middlewares/StudipMockNavigation.php
+++ b/lib/classes/JsonApi/Middlewares/StudipMockNavigation.php
@@ -18,53 +18,38 @@ class DummyNavigation extends \Navigation implements \ArrayAccess
/**
* ArrayAccess: Check whether the given offset exists.
- *
- * @todo Add bool return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetExists($offset)
+ public function offsetExists($offset): bool
{
return true;
}
/**
* ArrayAccess: Get the value at the given offset.
- *
- * @todo Add mixed return type when Stud.IP requires PHP8 minimal
*/
- #[\ReturnTypeWillChange]
- public function offsetGet($offset)
+ public function offsetGet($offset): mixed
{
return $this;
}
/**
* ArrayAccess: Set the value at the given offset.
- *
- * @todo Add void return type when Stud.IP requires PHP8 minimal
*/
- #[\ReturnTypeWillChange]
- public function offsetSet($offset, $value)
+ public function offsetSet($offset, $value): void
{
}
/**
* ArrayAccess: Delete the value at the given offset.
- *
- * @todo Add void return type when Stud.IP requires PHP8 minimal
*/
- #[\ReturnTypeWillChange]
- public function offsetUnset($offset)
+ public function offsetUnset($offset): void
{
}
/**
- * IteratorAggregate: Create interator for request parameters.
- *
- * @todo Add \Traversable return type when Stud.IP requires PHP8 minimal
+ * IteratorAggregate: Create iterator for request parameters.
*/
- #[\ReturnTypeWillChange]
- public function getIterator()
+ public function getIterator(): \Traversable
{
return new \ArrayIterator();
}
diff --git a/lib/classes/JsonApi/NonJsonApiController.php b/lib/classes/JsonApi/NonJsonApiController.php
index 8384b54..bfc51af 100644
--- a/lib/classes/JsonApi/NonJsonApiController.php
+++ b/lib/classes/JsonApi/NonJsonApiController.php
@@ -49,11 +49,11 @@ class NonJsonApiController
}
/**
- * @return mixed
+ * @return null|\User
*/
protected function getUser(Request $request)
{
- return $request->getAttribute(Authentication::USER_KEY, null);
+ return $request->getAttribute(Authentication::USER_KEY);
}
/**
diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php
index 4f44165..63c69e6 100644
--- a/lib/classes/JsonApi/RouteMap.php
+++ b/lib/classes/JsonApi/RouteMap.php
@@ -5,8 +5,7 @@ namespace JsonApi;
use JsonApi\Contracts\JsonApiPlugin;
use JsonApi\Middlewares\Authentication;
use JsonApi\Middlewares\DangerousRouteHandler;
-use JsonApi\Middlewares\JsonApi as JsonApiMiddleware;
-use JsonApi\Middlewares\StudipMockNavigation;
+use JsonApi\Routes\Consultations\SlotCreationCount;
use JsonApi\Routes\Holidays\HolidaysShow;
use Slim\Routing\RouteCollectorProxy;
@@ -49,7 +48,6 @@ use Slim\Routing\RouteCollectorProxy;
*
* $this->app->post('/article/{id}/comments', MeineRoute::class);
*
- * @see \JsonApi\Middlewares\JsonApi
* @see \JsonApi\Middlewares\Authentication
* @see \JsonApi\Contracts\JsonApiPlugin
* @see http://www.slimframework.com/docs/objects/router.html#how-to-create-routes
@@ -118,11 +116,12 @@ class RouteMap
$group->get('/status-groups/{id}', Routes\StatusgroupShow::class);
$this->addAuthenticatedBlubberRoutes($group);
+ $this->addAuthenticatedClipboardRoutes($group);
$this->addAuthenticatedConsultationRoutes($group);
$this->addAuthenticatedContactsRoutes($group);
$this->addAuthenticatedCoursesRoutes($group);
- if (\PluginManager::getInstance()->getPlugin('CoursewareModule')) {
+ if (\PluginManager::getInstance()->getPlugin(\CoursewareModule::class)) {
$this->addAuthenticatedCoursewareRoutes($group);
}
@@ -155,7 +154,7 @@ class RouteMap
$group->get('/studip/properties', Routes\Studip\PropertiesIndex::class);
- if (\PluginManager::getInstance()->getPlugin('CoursewareModule')) {
+ if (\PluginManager::getInstance()->getPlugin(\CoursewareModule::class)) {
$group->get('/public/courseware/{link_id}/courseware-structural-elements/{id}', Routes\Courseware\PublicStructuralElementsShow::class);
$group->get('/public/courseware/{link_id}/courseware-structural-elements', Routes\Courseware\PublicStructuralElementsIndex::class);
}
@@ -205,8 +204,26 @@ class RouteMap
);
}
+ private function addAuthenticatedClipboardRoutes(RouteCollectorProxy $group): void
+ {
+ $group->post('/clipboards', Routes\Clipboards\ClipboardsCreate::class);
+ $group->patch('/clipboards/{id}', Routes\Clipboards\ClipboardsUpdate::class);
+ $group->delete('/clipboards/{id}', Routes\Clipboards\ClipboardsDelete::class);
+
+ $group->get('/clipboard-items/{id}', Routes\Clipboards\ClipboardItemsShow::class);
+ $group->post('/clipboards/{id}/items', Routes\Clipboards\ClipboardItemsCreate::class);
+ $group->delete('/clipboards/{id}/items', Routes\Clipboards\ClipboardItemsDelete::class);
+ $group->delete('/clipboards/{id}/items/{itemId}', Routes\Clipboards\ClipboardItemsDelete::class);
+
+ $group->post('/clipboard-items', Routes\Clipboards\ClipboardItemsCreate::class);
+ $group->delete('/clipboard-items/{id}', Routes\Clipboards\ClipboardItemsDelete::class);
+ }
+
private function addAuthenticatedConsultationRoutes(RouteCollectorProxy $group): void
{
+ // TODO: I know, not very JSONAPI-like but it's a NonJsonApiController ¯\_(ツ)_/¯
+ $group->get('/consultation-slots/count', SlotCreationCount::class);
+
$group->get('/{type:courses|institutes|users}/{id}/consultations', Routes\Consultations\BlocksByRangeIndex::class);
$group->get('/consultation-blocks/{id}', Routes\Consultations\BlockShow::class);
diff --git a/lib/classes/JsonApi/Routes/Clipboards/Authority.php b/lib/classes/JsonApi/Routes/Clipboards/Authority.php
new file mode 100644
index 0000000..5cc053a
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Clipboards/Authority.php
@@ -0,0 +1,28 @@
+<?php
+namespace JsonApi\Routes\Clipboards;
+
+use User;
+
+final class Authority
+{
+ public static function canCreateClipboard(User $user): bool
+ {
+ return true;
+ }
+
+ public static function canAccessClipboard(User $user, \Clipboard $clipboard): bool
+ {
+ return $user->id === $clipboard->user_id
+ || $user->perms === 'root';
+ }
+
+ public static function canUpdateClipboard(User $user, \Clipboard $clipboard): bool
+ {
+ return self::canAccessClipboard($user, $clipboard);
+ }
+
+ public static function canDeleteClipboard(User $user, \Clipboard $clipboard): bool
+ {
+ return self::canUpdateClipboard($user, $clipboard);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsCreate.php b/lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsCreate.php
new file mode 100644
index 0000000..d57d0c5
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsCreate.php
@@ -0,0 +1,106 @@
+<?php
+namespace JsonApi\Routes\Clipboards;
+
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\ValidationTrait;
+use JsonApi\Schemas\Clipboard;
+use Psr\Http\Message\{
+ ResponseInterface as Response,
+ ServerRequestInterface as Request
+};
+
+final class ClipboardItemsCreate extends JsonApiController
+{
+ use ValidationTrait;
+
+ public function __invoke(Request $request, Response $response, $args): Response
+ {
+ $json = $this->validate($request, $args);
+
+ $clipboard_id = $args['id'] ?? $json['data']['relationships']['clipboard']['data']['id'];
+ $clipboard = \Clipboard::find($clipboard_id);
+
+ $user = $this->getUser($request);
+ if (!Authority::canUpdateClipboard($user, $clipboard)) {
+ throw new AuthorizationFailedException();
+ }
+
+ $range_id = $json['data']['attributes']['range_id'];
+ $range_type = $json['data']['attributes']['range_type'];
+
+ $item = \ClipboardItem::findOneBySql(
+ 'clipboard_id = ? AND range_id = ? AND range_type = ?',
+ [$clipboard_id, $range_id, $range_type]
+ );
+
+ if ($item) {
+ return $this->getCodeResponse(302, [
+ 'Location' => $this->getLinkToItem($item),
+ ]);
+ }
+
+ $item = \ClipboardItem::create([
+ 'clipboard_id' => $clipboard_id,
+ 'range_id' => $range_id,
+ 'range_type' => $range_type,
+ ]);
+
+ return $this->getContentResponse($item);
+ }
+
+ protected function validateResourceDocument($json, $data)
+ {
+ $clipboardValidationError = $this->validateRequestContainsValidClipboard($json, $data);
+ if ($clipboardValidationError !== null) {
+ return $clipboardValidationError;
+ }
+
+ if (!self::arrayHas($json, 'data.attributes.range_id')) {
+ return 'No range_id defined';
+ }
+
+ if (!self::arrayHas($json, 'data.attributes.range_type')) {
+ return 'No range_type defined';
+ }
+
+ $range_type = self::arrayGet($json, 'data.attributes.range_type');
+ if (!is_a($range_type, \StudipItem::class, true)) {
+ return 'Range type must implement interface StudipItem';
+ }
+
+ return null;
+ }
+
+ private function validateRequestContainsValidClipboard($json, $data): ?string
+ {
+ if (isset($data['id'])) {
+ if (!\Clipboard::exists($data['id'])) {
+ return 'Provided clipboard id is invalid';
+ }
+ } else {
+ if (!self::arrayHas($json, 'data.relationships.clipboard')) {
+ return 'No clipboard relationship defined';
+ }
+
+ $clipboard = self::arrayGet($json, 'data.relationships.clipboard');
+ if (
+ !isset($clipboard['data']['type'], $clipboard['data']['id'])
+ || $clipboard['data']['type'] !== Clipboard::TYPE
+ ) {
+ return 'Defined clipboard relationship has invalid format.';
+ }
+ if (!\Clipboard::exists($clipboard['data']['id'])) {
+ return 'Related clipboard does not exist.';
+ }
+ }
+
+ return null;
+ }
+
+ private function getLinkToItem(\ClipboardItem $item): string
+ {
+ $json = $this->encoder->encodeData($item);
+ return json_decode($json, true)['data']['links']['self'];
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsDelete.php b/lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsDelete.php
new file mode 100644
index 0000000..a9c7cd4
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsDelete.php
@@ -0,0 +1,54 @@
+<?php
+namespace JsonApi\Routes\Clipboards;
+
+use JsonApi\Errors\BadRequestException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\{
+ ResponseInterface as Response,
+ ServerRequestInterface as Request
+};
+
+final class ClipboardItemsDelete extends JsonApiController
+{
+ protected $allowedFilteringParameters = ['range_id'];
+
+ public function __invoke(Request $request, Response $response, $args): Response
+ {
+ $clipboard = \Clipboard::find($args['id']);
+ if (!$clipboard) {
+ throw new RecordNotFoundException('Clipboard not found');
+ }
+
+ $user = $this->getUser($request);
+ if (!Authority::canUpdateClipboard($user, $clipboard)) {
+ throw new \AccessDeniedException();
+ }
+
+ $item = null;
+ if (isset($args['itemId'])) {
+ $item = \ClipboardItem::find($args['itemId']);
+ } else {
+ $filtering = iterator_to_array($this->getQueryParameters()->getFilters());
+ if (!isset($filtering['range_id'])) {
+ throw new BadRequestException('No range_id filter given');
+ }
+ $item = \ClipboardItem::findOneBySQL(
+ 'clipboard_id = ? AND range_id = ?',
+ [$clipboard->id, $filtering['range_id']]
+ );
+ }
+
+ if (!$item) {
+ throw new RecordNotFoundException('Item not found');
+ }
+
+ if ($item->clipboard_id !== $clipboard->id) {
+ throw new BadRequestException('Item does not belong to clipboard');
+ }
+
+ $item->delete();
+
+ return $this->getCodeResponse(204);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsShow.php b/lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsShow.php
new file mode 100644
index 0000000..3c91708
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsShow.php
@@ -0,0 +1,28 @@
+<?php
+namespace JsonApi\Routes\Clipboards;
+
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\{
+ ResponseInterface as Response,
+ ServerRequestInterface as Request
+};
+
+final class ClipboardItemsShow extends JsonApiController
+{
+ public function __invoke(Request $request, Response $response, $args): Response
+ {
+ $item = \ClipboardItem::find($args['id']);
+ if (!$item) {
+ throw new RecordNotFoundException();
+ }
+
+ $user = $this->getUser($request);
+ if (!Authority::canAccessClipboard($user, $item->clipboard)) {
+ throw new AuthorizationFailedException();
+ }
+
+ return $this->getContentResponse($item);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Clipboards/ClipboardsCreate.php b/lib/classes/JsonApi/Routes/Clipboards/ClipboardsCreate.php
new file mode 100644
index 0000000..57fd9b9
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Clipboards/ClipboardsCreate.php
@@ -0,0 +1,46 @@
+<?php
+namespace JsonApi\Routes\Clipboards;
+
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\ValidationTrait;
+use Psr\Http\Message\{
+ ResponseInterface as Response,
+ ServerRequestInterface as Request
+};
+
+final class ClipboardsCreate extends JsonApiController
+{
+ use ValidationTrait;
+
+ public function __invoke(Request $request, Response $response, $args): Response
+ {
+ $user = $this->getUser($request);
+
+ if (!Authority::canCreateClipboard($user)) {
+ throw new AuthorizationFailedException();
+ }
+
+ $json = $this->validate($request, $args);
+
+ $clipboard = \Clipboard::create([
+ 'name' => $json['data']['attributes']['name'],
+ 'user_id' => $user->id,
+ ]);
+
+ return $this->getContentResponse($clipboard);
+ }
+
+ protected function validateResourceDocument($json, $data)
+ {
+ if (!self::arrayHas($json, 'data.attributes.name')) {
+ return 'No name for the clipboard defined';
+ }
+
+ if (!trim(self::arrayGet($json, 'data.attributes.name'))) {
+ return 'Name of the clipboard may not be empty';
+ }
+
+ return null;
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Clipboards/ClipboardsDelete.php b/lib/classes/JsonApi/Routes/Clipboards/ClipboardsDelete.php
new file mode 100644
index 0000000..0897843
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Clipboards/ClipboardsDelete.php
@@ -0,0 +1,31 @@
+<?php
+namespace JsonApi\Routes\Clipboards;
+
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\{
+ ResponseInterface as Response,
+ ServerRequestInterface as Request
+};
+
+final class ClipboardsDelete extends JsonApiController
+{
+ public function __invoke(Request $request, Response $response, $args): Response
+ {
+ $clipboard = \Clipboard::find($args['id']);
+ if (!$clipboard) {
+ throw new RecordNotFoundException();
+ }
+
+ $user = $this->getUser($request);
+
+ if (!Authority::canDeleteClipboard($user, $clipboard)) {
+ throw new AuthorizationFailedException();
+ }
+
+ $clipboard->delete();
+
+ return $this->getCodeResponse(204);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Clipboards/ClipboardsUpdate.php b/lib/classes/JsonApi/Routes/Clipboards/ClipboardsUpdate.php
new file mode 100644
index 0000000..83d9539
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Clipboards/ClipboardsUpdate.php
@@ -0,0 +1,50 @@
+<?php
+namespace JsonApi\Routes\Clipboards;
+
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\ValidationTrait;
+use Psr\Http\Message\{
+ ResponseInterface as Response,
+ ServerRequestInterface as Request
+};
+
+final class ClipboardsUpdate extends JsonApiController
+{
+ use ValidationTrait;
+
+ public function __invoke(Request $request, Response $response, $args): Response
+ {
+ $clipboard = \Clipboard::find($args['id']);
+ if (!$clipboard) {
+ throw new RecordNotFoundException();
+ }
+
+ $user = $this->getUser($request);
+
+ if (!Authority::canUpdateClipboard($user, $clipboard)) {
+ throw new AuthorizationFailedException();
+ }
+
+ $json = $this->validate($request, $args);
+
+ $clipboard->name = $json['data']['attributes']['name'];
+ $clipboard->store();
+
+ return $this->getContentResponse($clipboard);
+ }
+
+ protected function validateResourceDocument($json, $data)
+ {
+ if (!self::arrayHas($json, 'data.attributes.name')) {
+ return 'No name for the clipboard defined';
+ }
+
+ if (!trim(self::arrayGet($json, 'data.attributes.name'))) {
+ return 'Name of the clipboard may not be empty';
+ }
+
+ return null;
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Consultations/SlotCreationCount.php b/lib/classes/JsonApi/Routes/Consultations/SlotCreationCount.php
new file mode 100644
index 0000000..c378771
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Consultations/SlotCreationCount.php
@@ -0,0 +1,105 @@
+<?php
+namespace JsonApi\Routes\Consultations;
+
+use ConsultationBlock;
+use JsonApi\Errors\BadRequestException;
+use JsonApi\NonJsonApiController;
+use Neomerx\JsonApi\Exceptions\JsonApiException;
+use Neomerx\JsonApi\Schema\ErrorCollection;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+final class SlotCreationCount extends NonJsonApiController
+{
+ public function __invoke(Request $request, Response $response, array $args)
+ {
+ $parameters = $request->getQueryParams();
+
+ $this->validateParameters($parameters);
+
+ // Determine duration of a slot and pause times
+ $slot_count = ConsultationBlock::countSlots(
+ strtotime($parameters['start']),
+ strtotime($parameters['end']),
+ $parameters['dow'],
+ $parameters['interval'],
+ $parameters['duration'],
+ $parameters['pause_time'] ?? null,
+ $parameters['pause_duration'] ?? null
+ );
+
+ $response->getBody()->write((string) $slot_count);
+ return $response->withAddedHeader('Content-Type', 'application/json');
+ }
+
+ private function validateParameters(array $parameters): void
+ {
+ $collection = new ErrorCollection();
+
+ foreach (['start', 'end', 'dow', 'interval', 'duration'] as $key) {
+ if (!isset($parameters[$key])) {
+ $collection->addQueryParameterError($key, 'Parameter is missing');
+ }
+ }
+
+ if (isset($parameters['start'], $parameters['end'])) {
+ $start = strtotime($parameters['start']);
+ $end = strtotime($parameters['end']);
+
+ if (!$start) {
+ $collection->addQueryParameterError('start', 'Parameter has invalid datetime format');
+ }
+
+ if (!$end) {
+ $collection->addQueryParameterError('end', 'Parameter has invalid datetime format');
+ }
+
+ if ($start && $end && $start > $end) {
+ $collection->addQueryParameterError('start', 'Datetime value of start must be before end');
+ }
+ }
+
+ if (
+ isset($parameters['dow'])
+ && (
+ !ctype_digit($parameters['dow'])
+ || $parameters['dow'] < 0
+ || $parameters['dow'] > 6
+ )
+ ) {
+ $collection->addQueryParameterError('dow', 'Parameter must be a number between 0 and 6');
+ }
+
+ if (
+ isset($parameters['interval'])
+ && (
+ !ctype_digit($parameters['interval'])
+ || $parameters['interval'] < 0
+ || $parameters['interval'] > 4
+ )
+ ) {
+ $collection->addQueryParameterError('interval', 'Parameter must be a number between 0 and 4');
+ }
+
+ if (
+ isset($parameters['duration'])
+ && (
+ !ctype_digit($parameters['duration'])
+ || $parameters['duration'] <= 0
+ )
+ ) {
+ $collection->addQueryParameterError('duration', 'Parameter must be a positive number');
+ }
+
+ if (
+ isset($parameters['pause_time'], $parameters['duration'])
+ && $parameters['pause_time'] < $parameters['duration']
+ ) {
+ $collection->addQueryParameterError('pause_time', 'The defined time to a pause is shorter than the duration of a slot.');
+ }
+
+ if (count($collection) > 0) {
+ throw new JsonApiException($collection);
+ }
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php b/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php
index e676507..d913966 100644
--- a/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php
+++ b/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php
@@ -100,6 +100,20 @@ class UnitsCreate extends JsonApiController
'commentable' => 0
]);
+ \Courseware\Container::create([
+ 'structural_element_id' => $struct->id,
+ 'owner_id' => $user->id,
+ 'editor_id' => $user->id,
+ 'edit_blocker_id' => '',
+ 'position' => 0,
+ 'container_type' => 'list',
+
+ 'payload' => json_encode([
+ 'colspan' => 'full',
+ 'sections' => [['name' => _('erstes Element'), 'icon' => '','blocks' => []]]
+ ]),
+ ]);
+
$unit = \Courseware\Unit::create([
'range_id' => $range->getRangeId(),
'range_type' => $range->getRangeType(),
diff --git a/lib/classes/JsonApi/Routes/Files/RangeFileRefsIndex.php b/lib/classes/JsonApi/Routes/Files/RangeFileRefsIndex.php
index 8f69d6a..773071e 100644
--- a/lib/classes/JsonApi/Routes/Files/RangeFileRefsIndex.php
+++ b/lib/classes/JsonApi/Routes/Files/RangeFileRefsIndex.php
@@ -13,7 +13,7 @@ class RangeFileRefsIndex extends AbstractRangeIndex
$filerefs = [];
foreach ($filesAndFolders['files'] as $file_object) {
- if (method_exists($file_object, "getFileRef")) {
+ if (method_exists($file_object, 'getFileRef')) {
$filerefs[] = $file_object->getFileRef();
}
}
diff --git a/lib/classes/JsonApi/Routes/Files/SubfilerefsIndex.php b/lib/classes/JsonApi/Routes/Files/SubfilerefsIndex.php
index 0ff0603..2ed1a23 100644
--- a/lib/classes/JsonApi/Routes/Files/SubfilerefsIndex.php
+++ b/lib/classes/JsonApi/Routes/Files/SubfilerefsIndex.php
@@ -2,6 +2,7 @@
namespace JsonApi\Routes\Files;
+use FileRef;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use JsonApi\Errors\AuthorizationFailedException;
@@ -28,8 +29,14 @@ class SubfilerefsIndex extends JsonApiController
throw new AuthorizationFailedException();
}
- $fileRefs = $folder->file_refs->getArrayCopy();
- list($offset, $limit) = $this->getOffsetAndLimit();
+ $fileRefs = array_map(
+ function (\FileType $file): FileRef {
+ return $file->getFileRef();
+ },
+ $folder->getFiles()
+ );
+
+ [$offset, $limit] = $this->getOffsetAndLimit();
return $this->getPaginatedContentResponse(
array_slice($fileRefs, $offset, $limit),
diff --git a/lib/classes/JsonApi/Routes/Files/SubfoldersIndex.php b/lib/classes/JsonApi/Routes/Files/SubfoldersIndex.php
index e8f4d13..f0ad18c 100644
--- a/lib/classes/JsonApi/Routes/Files/SubfoldersIndex.php
+++ b/lib/classes/JsonApi/Routes/Files/SubfoldersIndex.php
@@ -19,20 +19,31 @@ class SubfoldersIndex extends JsonApiController
*/
public function __invoke(Request $request, Response $response, $args)
{
- if (!$folder = \FileManager::getTypedFolder($args['id'])) {
+ $folder = \FileManager::getTypedFolder($args['id']);
+ if (!$folder) {
throw new RecordNotFoundException();
}
- if (!Authority::canShowFolder($this->getUser($request), $folder)) {
+ $user = $this->getUser($request);
+
+ if (!Authority::canShowFolder($user, $folder)) {
throw new AuthorizationFailedException();
}
- $subfolders = array_map(
- function ($subfolder) {
- return $subfolder->getTypedFolder();
+ $subfolders = array_reduce(
+ $folder->subfolders->getArrayCopy(),
+ function ($result, $subfolder) use ($user) {
+ $folder = $subfolder->getTypedFolder();
+
+ if (Authority::canShowFolder($user, $folder)) {
+ $result[] = $folder;
+ }
+
+ return $result;
},
- $folder->subfolders->getArrayCopy()
+ []
);
+
list($offset, $limit) = $this->getOffsetAndLimit();
return $this->getPaginatedContentResponse(
diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php
index 97212bc..1498daf 100644
--- a/lib/classes/JsonApi/SchemaMap.php
+++ b/lib/classes/JsonApi/SchemaMap.php
@@ -2,6 +2,8 @@
namespace JsonApi;
+use JsonApi\Schemas\Clipboard;
+
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
@@ -19,6 +21,8 @@ class SchemaMap
\BlubberThread::class => Schemas\BlubberThread::class,
\CalendarDateAssignment::class => Schemas\CalendarDateAssignment::class,
+ \Clipboard::class => Schemas\Clipboard::class,
+ \ClipboardItem::class => Schemas\ClipboardItem::class,
\ConsultationBlock::class => Schemas\ConsultationBlock::class,
\ConsultationBooking::class => Schemas\ConsultationBooking::class,
\ConsultationSlot::class => Schemas\ConsultationSlot::class,
diff --git a/lib/classes/JsonApi/Schemas/Clipboard.php b/lib/classes/JsonApi/Schemas/Clipboard.php
new file mode 100644
index 0000000..af90d73
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/Clipboard.php
@@ -0,0 +1,81 @@
+<?php
+namespace JsonApi\Schemas;
+
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+final class Clipboard extends SchemaProvider
+{
+ public const TYPE = 'clipboards';
+ public const REL_USER = 'user';
+ public const REL_ITEMS = 'clipboard-items';
+
+ /**
+ * @param \Clipboard $resource
+ */
+ public function getId($resource): ?string
+ {
+ return (string) $resource->id;
+ }
+
+ /**
+ * @param \Clipboard $resource
+ */
+ public function getAttributes($resource, ContextInterface $context): iterable
+ {
+ return [
+ 'name' => $resource->name,
+ 'handler' => $resource->handler,
+ 'allows_item_class' => $resource->allowed_item_class,
+ 'mkdate' => date('c', $resource->mkdate),
+ 'chdate' => date('c', $resource->chdate),
+ ];
+ }
+
+ /**
+ * @param \Clipboard $resource
+ */
+ public function getRelationships($resource, ContextInterface $context): iterable
+ {
+ $relationships = [];
+
+ $isPrimary = $context->getPosition()->getLevel() === 0;
+ if ($isPrimary) {
+ $relationships = $this->getUserRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_USER));
+ $relationships = $this->getItemsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_ITEMS));
+ }
+
+
+ return $relationships;
+ }
+
+ private function getUserRelationship(array $relationships, \Clipboard $clipboard, bool $includeData): array
+ {
+ $relationships[self::REL_USER] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($clipboard->user),
+ ],
+ self::RELATIONSHIP_DATA => $includeData ? $clipboard->user : \User::build(['id' => $clipboard->user_id], false),
+ ];
+
+ return $relationships;
+ }
+
+ private function getItemsRelationship(array $relationships, \Clipboard $clipboard, bool $includeData): array
+ {
+ if ($includeData) {
+ $relatedItems = $clipboard->items;
+ } else {
+ $relatedItems = $clipboard->items->map(fn($item) => \ClipboardItem::build(['id' => $item->id], false));
+ }
+
+ $relationships[self::REL_ITEMS] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->getRelationshipRelatedLink($clipboard, self::REL_ITEMS),
+ ],
+ self::RELATIONSHIP_DATA => $relatedItems,
+ ];
+
+ return $relationships;
+ }
+}
diff --git a/lib/classes/JsonApi/Schemas/ClipboardItem.php b/lib/classes/JsonApi/Schemas/ClipboardItem.php
new file mode 100644
index 0000000..9c84823
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/ClipboardItem.php
@@ -0,0 +1,61 @@
+<?php
+namespace JsonApi\Schemas;
+
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+final class ClipboardItem extends SchemaProvider
+{
+ public const TYPE = 'clipboard-items';
+ public const REL_CLIPBOARD = 'clipboard';
+
+ /**
+ * @param \ClipboardItem $resource
+ */
+ public function getId($resource): ?string
+ {
+ return (string) $resource->id;
+ }
+
+ /**
+ * @param \ClipboardItem $resource
+ */
+ public function getAttributes($resource, ContextInterface $context): iterable
+ {
+ return [
+ 'range_id' => $resource->range_id,
+ 'range_type' => $resource->range_type,
+ 'name' => $resource->name,
+ 'mkdate' => date('c', $resource->mkdate),
+ 'chdate' => date('c', $resource->chdate),
+ ];
+ }
+
+ /**
+ * @param \ClipboardItem $resource
+ */
+ public function getRelationships($resource, ContextInterface $context): iterable
+ {
+ $relationships = [];
+
+ $isPrimary = $context->getPosition()->getLevel() === 0;
+ if ($isPrimary) {
+ $relationships = $this->getClipboardRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_CLIPBOARD));
+ }
+
+
+ return $relationships;
+ }
+
+ private function getClipboardRelationship(array $relationships, \ClipboardItem $clipboardItem, bool $includeData): array
+ {
+ $relationships[self::REL_CLIPBOARD] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($clipboardItem->clipboard),
+ ],
+ self::RELATIONSHIP_DATA => $includeData ? $clipboardItem->clipboard : \User::build(['id' => $clipboardItem->clipboard_id], false),
+ ];
+
+ return $relationships;
+ }
+}
diff --git a/lib/classes/JsonApi/Schemas/File.php b/lib/classes/JsonApi/Schemas/File.php
index 8eb8046..df0263a 100644
--- a/lib/classes/JsonApi/Schemas/File.php
+++ b/lib/classes/JsonApi/Schemas/File.php
@@ -29,7 +29,7 @@ class File extends SchemaProvider
'chdate' => date('c', $resource['chdate']),
];
- if ($resource['metadata']['url']) {
+ if (!empty($resource['metadata']['url'])) {
if (FilesAuthority::canUpdateFile($this->currentUser, $resource)) {
$attributes['url'] = $resource['metadata']['url'];
}
diff --git a/lib/classes/JsonApi/Schemas/Folder.php b/lib/classes/JsonApi/Schemas/Folder.php
index 2c61cae..4cb277e 100644
--- a/lib/classes/JsonApi/Schemas/Folder.php
+++ b/lib/classes/JsonApi/Schemas/Folder.php
@@ -169,14 +169,24 @@ class Folder extends SchemaProvider
return $relationships;
}
+ /**
+ * @param \FolderType $resource
+ */
private function getFilesRelationship(array $relationships, $resource)
{
+ $fileRefs = array_map(
+ function (\FileType $file): \FileRef {
+ return $file->getFileRef();
+ },
+ $resource->getFiles()
+ );
+
$relationships[self::REL_FILE_REFS] = [
self::RELATIONSHIP_LINKS => [
Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_FILE_REFS),
],
self::RELATIONSHIP_META => [
- 'count' => count($resource->file_refs)
+ 'count' => count($fileRefs),
],
];
diff --git a/lib/classes/JsonApi/Schemas/WikiPage.php b/lib/classes/JsonApi/Schemas/WikiPage.php
index 857666e..f061ecc 100644
--- a/lib/classes/JsonApi/Schemas/WikiPage.php
+++ b/lib/classes/JsonApi/Schemas/WikiPage.php
@@ -152,7 +152,7 @@ class WikiPage extends SchemaProvider
*/
private function addAuthorRelationship($relationships, $wiki, $includeList)
{
- if ($wiki->user_id) {
+ if ($wiki->user_id && $wiki->user_id !== 'nobody') {
$relationships[self::REL_AUTHOR] = [
self::RELATIONSHIP_LINKS => [
Link::RELATED => $this->createLinkToResource($wiki->user),
diff --git a/lib/classes/LayoutMessage.interface.php b/lib/classes/LayoutMessage.php
index 7072788..7072788 100644
--- a/lib/classes/LayoutMessage.interface.php
+++ b/lib/classes/LayoutMessage.php
diff --git a/lib/classes/LinkButton.class.php b/lib/classes/LinkButton.php
index 40044b6..40044b6 100644
--- a/lib/classes/LinkButton.class.php
+++ b/lib/classes/LinkButton.php
diff --git a/lib/classes/LockRules.class.php b/lib/classes/LockRules.php
index 59679db..f1feb9f 100644
--- a/lib/classes/LockRules.class.php
+++ b/lib/classes/LockRules.php
@@ -1,6 +1,6 @@
<?php
/**
- * LockRules.class.php
+ * LockRules.php
*
*
* This program is free software; you can redistribute it and/or
@@ -16,7 +16,7 @@
*/
/**
-* LockRules.class.php
+* LockRules.php
*
* This class contains only static methods dealing with lock rules
*
diff --git a/lib/classes/Loggable.class.php b/lib/classes/Loggable.php
index ed0e162..ed0e162 100644
--- a/lib/classes/Loggable.class.php
+++ b/lib/classes/Loggable.php
diff --git a/lib/classes/LtiLink.php b/lib/classes/LtiLink.php
index 801b6c0..1423cef 100644
--- a/lib/classes/LtiLink.php
+++ b/lib/classes/LtiLink.php
@@ -310,12 +310,14 @@ class LtiLink
// posted form data will always use CR LF
$launch_params = preg_replace("/\r?\n/", "\r\n", $launch_params);
- // In OAuth, request parameters must be sorted by name
- ksort($launch_params);
- $launch_params = http_build_query($launch_params, '', '&', PHP_QUERY_RFC3986);
- $base_string = 'POST&' . rawurlencode($launch_url) . '&' . rawurlencode($launch_params);
- $secret = rawurlencode($this->consumer_secret) . '&';
-
- return base64_encode(hash_hmac($this->oauth_signature_method, $base_string, $secret, true));
+ return Studip\OAuth1::signRequest(
+ (new Slim\Psr7\Factory\ServerRequestFactory())->createServerRequest(
+ 'POST',
+ $launch_url
+ )->withQueryParams($launch_params),
+ $this->consumer_secret,
+ '',
+ $this->oauth_signature_method
+ );
}
}
diff --git a/lib/classes/MVV.class.php b/lib/classes/MVV.php
index 67bbbeb..b4d9edf 100644
--- a/lib/classes/MVV.class.php
+++ b/lib/classes/MVV.php
@@ -1,6 +1,6 @@
<?php
/**
- * MVV.class.php
+ * MVV.php
* Helper class
*
* This program is free software; you can redistribute it and/or
@@ -843,7 +843,7 @@ class MVV implements Loggable {
public static function getContentLanguageImagePath($language): string
{
$content_language = $GLOBALS['MVV_MODUL_DESKRIPTOR']['SPRACHE']['values'][$language]['content_language'];
- return 'languages/' . ($GLOBALS['CONTENT_LANGUAGES'][$content_language]?$GLOBALS['CONTENT_LANGUAGES'][$content_language]['picture']:'lang_' . mb_strtolower($language) . '.gif');
+ return 'languages/' . ($GLOBALS['CONTENT_LANGUAGES'][$content_language]['picture'] ?? 'lang_' . mb_strtolower($language) . '.gif');
}
}
diff --git a/lib/classes/Markup.class.php b/lib/classes/Markup.php
index da040fa..dc6820f 100644
--- a/lib/classes/Markup.class.php
+++ b/lib/classes/Markup.php
@@ -1,6 +1,6 @@
<?php
/**
- * Markup.class.php - Handling of Stud.IP- and HTML-markup.
+ * Markup.php - Handling of Stud.IP- and HTML-markup.
**
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
@@ -363,7 +363,8 @@ class Markup
'border-color',
'border-style',
'float',
- 'border'
+ 'border',
+ 'vertical-align'
]);
$config->set('CSS.MaxImgLength', null);
diff --git a/lib/classes/MessageBox.class.php b/lib/classes/MessageBox.php
index 450851f..3573018 100644
--- a/lib/classes/MessageBox.class.php
+++ b/lib/classes/MessageBox.php
@@ -1,7 +1,7 @@
<?php
# Lifter010: TODO
/**
- * MessageBox.class.php
+ * MessageBox.php
*
* html boxes for different kinds of messages
*
@@ -41,6 +41,7 @@ class MessageBox implements LayoutMessage
public $details;
public $close_details;
protected $hide_close = false;
+ public static $counter = 0;
/**
* This function returns an exception message box. Use it only for system errors
@@ -141,18 +142,36 @@ class MessageBox implements LayoutMessage
}
/**
+ * Return whether this messagebox can be closed or not.
+ * @return bool
+ */
+ public function isCloseable(): bool
+ {
+ return $this->hide_close;
+ }
+
+ /**
* This method renders a MessageBox object to a string.
*
* @return string html output of the message box
*/
public function __toString()
{
+ $label = [
+ 'exception' => _('Systemfehler'),
+ 'error' => _('Fehler'),
+ 'warning' => _('Warnung'),
+ 'info' => _('Hinweis'),
+ 'success' => _('Erfolg'),
+ ];
return $GLOBALS['template_factory']->render('shared/message_box', [
'class' => $this->class,
'message' => $this->message,
'details' => is_array($this->details) ? $this->details : [],
'close_details' => $this->close_details,
'hide_close' => $this->hide_close,
+ 'label' => $label[$this->class],
+ 'counter' => self::$counter++,
]);
}
}
diff --git a/lib/classes/Metrics.php b/lib/classes/Metrics.php
index c3874d5..e6a2925 100644
--- a/lib/classes/Metrics.php
+++ b/lib/classes/Metrics.php
@@ -191,7 +191,7 @@ class Metrics {
// cache the activated MetricsPlugins
if (!self::$metricPlugins) {
- self::$metricPlugins = \PluginEngine::getPlugins('MetricsPlugin');
+ self::$metricPlugins = \PluginEngine::getPlugins(MetricsPlugin::class);
}
// call every MetricPlugin
diff --git a/lib/classes/ModulesNotification.class.php b/lib/classes/ModulesNotification.php
index 324b5c5..1dc361c 100644
--- a/lib/classes/ModulesNotification.class.php
+++ b/lib/classes/ModulesNotification.php
@@ -4,7 +4,7 @@
# Lifter003: TEST
# Lifter010: DONE - no html output in this file
/**
-* ModulesNotification.class.php
+* ModulesNotification.php
*
* check for modules (global and local for institutes and Veranstaltungen), read and write
*
@@ -17,7 +17,7 @@
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// Modules.class.php
+// Modules.php
// Checks fuer Module (global und lokal fuer Veranstaltungen und Einrichtungen), Schreib-/Lesezugriff
// Copyright (C) 2003 Cornelis Kater <ckater@gwdg.de>, Suchi & Berg GmbH <info@data-quest.de>
// +---------------------------------------------------------------------------+
@@ -126,7 +126,8 @@ class ModulesNotification
$navigation = MyRealmModel::getAdditionalNavigations($seminar_id, $s_data, null, $user_id, $visit_data[$seminar_id]);
$n_data = [];
foreach ($this->registered_notification_modules as $id => $m_data) {
- if (in_array($id, $s_data['notification'])
+ if (
+ in_array($id, $s_data['notification'])
&& isset($navigation[$id])
&& $navigation[$id]->getImage()
&& $navigation[$id]->getImage()->getRole() === Icon::ROLE_ATTENTION
@@ -142,12 +143,15 @@ class ModulesNotification
}
}
if (count($news)) {
- $auth_plugin = User::find($user_id)->auth_plugin;
+ $user = User::find($user_id);
+ $auth_plugin = $user->auth_plugin;
if (!is_a('StudipAuth' . ucfirst($auth_plugin), 'StudipAuthSSO', true)) {
$auth_plugin = null;
}
$template = $GLOBALS['template_factory']->open('mail/notification_html');
$template->set_attribute('lang', getUserLanguagePath($user_id));
+ $template->set_attribute('rec_fullname', $user->getFullname('full'));
+ $template->set_attribute('rec_username', $user->username);
$template->set_attribute('news', $news);
$template->set_attribute('sso', $auth_plugin);
@@ -165,14 +169,14 @@ class ModulesNotification
$base_url = URLHelper::setBaseURL('');
URLHelper::setBaseURl($base_url);
if ($nav instanceof Navigation && $nav->isVisible(true)) {
- $url = 'seminar_main.php?again=yes&auswahl=' . $seminar_id . '&redirect_to=' . strtr($nav->getURL(), '?', '&');
- $icon = $nav->getImage();
- $text = $nav->getTitle();
- if (!$text) {
- $text = $this->registered_notification_modules[$id]['name'];
- }
- $text .= ' - ' . $nav->getLinkAttributes()['title'];
- return compact('text', 'url', 'icon', 'seminar_id');
+ $url = 'seminar_main.php?again=yes&auswahl=' . $seminar_id . '&redirect_to=' . strtr($nav->getURL(), '?', '&');
+ $icon = $nav->getImage();
+ $text = $nav->getTitle();
+ if (!$text) {
+ $text = $this->registered_notification_modules[$id]['name'];
}
+ $text .= ' - ' . $nav->getLinkAttributes()['title'];
+ return compact('text', 'url', 'icon', 'seminar_id');
+ }
}
}
diff --git a/lib/classes/MultiDimArrayObject.class.php b/lib/classes/MultiDimArrayObject.php
index 459578f..b578018 100644
--- a/lib/classes/MultiDimArrayObject.class.php
+++ b/lib/classes/MultiDimArrayObject.php
@@ -78,13 +78,8 @@ class MultiDimArrayObject extends StudipArrayObject
/**
* Create a new iterator from an ArrayObject instance
- *
- * @return \Iterator
- *
- * @todo Add Traversable return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function getIterator()
+ public function getIterator(): Traversable
{
$class = $this->iteratorClass;
@@ -96,12 +91,8 @@ class MultiDimArrayObject extends StudipArrayObject
*
* @param mixed $key
* @param mixed $value
- * @return void
- *
- * @todo Add void return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetSet($key, $value)
+ public function offsetSet($key, $value): void
{
$new_value = $this->recursiveArrayToArrayObjects($value);
if (is_array($new_value)) {
diff --git a/lib/classes/MultiPersonSearch.class.php b/lib/classes/MultiPersonSearch.php
index 763ccd5..3ca617b 100644
--- a/lib/classes/MultiPersonSearch.class.php
+++ b/lib/classes/MultiPersonSearch.php
@@ -1,6 +1,6 @@
<?php
/**
- * MultiPersonSearch.class.php
+ * MultiPersonSearch.php
*
* This class provides a GUI-element for searching, adding and removing
* multiple persons. If JavaScript is enabled the GUI-element is shown
@@ -242,7 +242,7 @@ class MultiPersonSearch {
/**
* sets the search object.
*
- * @param SearchType object of type SearchType (e.g. SQLSearch.class.php)
+ * @param SearchType object of type SearchType (e.g. SQLSearch.php)
*
* @return MultiPersonSearch
*/
@@ -528,9 +528,9 @@ class MultiPersonSearch {
* sets default values of the internal variables.
*/
private function setDefaultValues() {
- $this->title = _('Personen hinzufügen');
+ $this->title = '';
$this->description = _('Bitte wählen Sie aus, wen Sie hinzufügen möchten.');
- $this->linkIconPath = Icon::create("add", "clickable", ['title' => _('Personen hinzufügen')]);
+ $this->linkIconPath = Icon::create('add');
}
/**
diff --git a/lib/classes/MvvPerm.php b/lib/classes/MvvPerm.php
index 93f9f44..91e1e35 100644
--- a/lib/classes/MvvPerm.php
+++ b/lib/classes/MvvPerm.php
@@ -563,7 +563,7 @@ class MvvPerm {
private static function getPrivileges($mvv_table)
{
if (self::$privileges === null) {
- $cache = StudipCacheFactory::getCache();
+ $cache = \Studip\Cache\Factory::getCache();
self::$privileges = unserialize($cache->read(MVV::CACHE_KEY . '/privileges'));
}
@@ -576,7 +576,7 @@ class MvvPerm {
include $config_file; // Defines $privileges
self::$privileges[$mvv_table] = $privileges ?? [];
}
- $cache = StudipCacheFactory::getCache();
+ $cache = \Studip\Cache\Factory::getCache();
$cache->write(MVV::CACHE_KEY . '/privileges', serialize(self::$privileges));
}
}
diff --git a/lib/classes/MvvQuickSearch.php b/lib/classes/MvvQuickSearch.php
index b0e79c0..b06de4f 100644
--- a/lib/classes/MvvQuickSearch.php
+++ b/lib/classes/MvvQuickSearch.php
@@ -1,6 +1,6 @@
<?php
-require_once 'lib/classes/searchtypes/SQLSearch.class.php';
+require_once 'lib/classes/searchtypes/SQLSearch.php';
class MvvQuickSearch extends SQLSearch
{
diff --git a/lib/classes/MyRealmModel.php b/lib/classes/MyRealmModel.php
index 8008580..8997e78 100644
--- a/lib/classes/MyRealmModel.php
+++ b/lib/classes/MyRealmModel.php
@@ -93,35 +93,6 @@ class MyRealmModel
}
}
- $sql = "SELECT COUNT(a.eval_id) as count,
- COUNT(IF((chdate > IFNULL(b.visitdate, :threshold) AND d.author_id !=:user_id ), a.eval_id, NULL)) AS neue,
- MAX(IF ((chdate > IFNULL(b.visitdate, :threshold) AND d.author_id != :user_id), chdate, 0)) AS last_modified
- FROM eval_range a
- INNER JOIN eval d
- ON (a.eval_id = d.eval_id AND d.startdate < UNIX_TIMESTAMP() AND (d.stopdate > UNIX_TIMESTAMP() OR d.startdate + d.timespan > UNIX_TIMESTAMP() OR (d.stopdate IS NULL AND d.timespan IS NULL)))
- LEFT JOIN object_user_visits b
- ON (b.object_id = a.eval_id AND b.user_id = :user_id AND b.plugin_id = :plugin_id)
- WHERE a.range_id = :course_id
- GROUP BY a.range_id";
-
- $statement = DBManager::get()->prepare($sql);
- $statement->bindValue(':user_id', $user_id);
- $statement->bindValue(':course_id', $object_id);
- $statement->bindValue(':threshold', object_get_visit_threshold());
- $statement->bindValue(':plugin_id', -2);
- $statement->execute();
- $result = $statement->fetch(PDO::FETCH_ASSOC);
- if (!empty($result)) {
- $count += $result['count'];
- $neue += $result['neue'];
- if (isset($my_obj['last_modified'], $result['last_modified']) && $result['last_modified']) {
- if ($my_obj['last_modified'] < $result['last_modified']) {
- $my_obj['last_modified'] = $result['last_modified'];
- }
- }
- }
-
-
if ($neue || $count > 0) {
$nav = new Navigation('vote', '#vote');
if ($neue) {
@@ -163,16 +134,22 @@ class MyRealmModel
public static function getCourses($min_sem_key, $max_sem_key, $params = [])
{
// init
- $order_by = $params['order_by'] ?? null;
- $order = $params['order'] ?? null;
- $deputies_enabled = $params['deputies_enabled'];
+ $order_by = $params['order_by'] ?? null;
+ $order = $params['order'] ?? null;
+ $deputies_enabled = $params['deputies_enabled'];
$sem_data = Semester::getAllAsArray();
$semester_ids = [];
if (is_numeric($min_sem_key) && is_numeric($max_sem_key)) {
foreach ($sem_data as $index => $data) {
- if ($index >= $min_sem_key && $index <= $max_sem_key) {
+ if (
+ $index >= $min_sem_key && $index <= $max_sem_key
+ && (
+ !isset($params['exactly'])
+ || in_array($index, $params['exactly'])
+ )
+ ) {
$semester_ids[] = $data['semester_id'] ?? '';
}
}
@@ -232,7 +209,9 @@ class MyRealmModel
$current_sem = null;
foreach ($sem_data as $sem_key => $one_sem) {
$current_sem = $sem_key;
- if (!$one_sem['past']) break;
+ if (!$one_sem['past']) {
+ break;
+ }
}
if (isset($sem_data[$current_sem + 1])) {
@@ -242,7 +221,7 @@ class MyRealmModel
}
// Get the needed semester
- if (!in_array($sem, ['', 'current', 'future', 'last', 'lastandnext'])) {
+ if (!in_array($sem, ['', 'current', 'future', 'last', 'lastandnext','lastbutone'])) {
$semesters[] = Semester::getIndexById($sem);
} else {
switch ($sem) {
@@ -262,6 +241,10 @@ class MyRealmModel
$semesters[] = $current_sem;
$semesters[] = $max_sem;
break;
+ case 'lastbutone':
+ $semesters[] = $current_sem - 2;
+ $semesters[] = $current_sem;
+ break;
default:
$semesters = array_keys($sem_data);
break;
@@ -281,11 +264,11 @@ class MyRealmModel
public static function getPreparedCourses($sem = '', $params = [])
{
$semesters = self::getSelectedSemesters($sem);
- $current_semester_nr = Semester::getIndexById(@Semester::findCurrent()->id);
+ $current_semester_nr = Semester::getIndexById(Semester::findCurrent()->id ?? null);
$min_sem_key = min($semesters);
$max_sem_key = max($semesters);
$group_field = $params['group_field'];
- $courses = self::getCourses($min_sem_key, $max_sem_key, $params);
+ $courses = self::getCourses($min_sem_key, $max_sem_key, $params + ['exactly' => $semesters]);
$show_semester_name = UserConfig::get($GLOBALS['user']->id)->SHOWSEM_ENABLE;
$sem_courses = [];
@@ -332,7 +315,7 @@ class MyRealmModel
$_course['visitdate'] = $visits[$course->id][0]['visitdate'];
$_course['user_status'] = $user_status;
$_course['gruppe'] = !$is_deputy ? $member_ships[$course->id]['gruppe'] ?? null : ($deputy ? $deputy->gruppe : null);
- $_course['sem_number_end'] = $course->isOpenEnded() ? $max_sem_key : Semester::getIndexById($course->end_semester->id);
+ $_course['sem_number_end'] = $course->isOpenEnded() ? $max_sem_key : Semester::getIndexById($course->end_semester->id ?? null);
$_course['sem_number'] = Semester::getIndexById($course->start_semester->id);
$_course['tools'] = $course->tools;
$_course['name'] = $course->name;
@@ -492,9 +475,9 @@ class MyRealmModel
public static function setObjectVisits($object, $user_id, $timestamp = null)
{
// load plugins, so they have a chance to register themselves as observers
- PluginEngine::getPlugins('StandardPlugin');
+ PluginEngine::getPlugins(StandardPlugin::class);
- // Update news, votes and evaluations
+ // Update news and votes
$query = "INSERT INTO object_user_visits
(object_id, user_id, plugin_id, visitdate, last_visitdate)
(
@@ -502,10 +485,6 @@ class MyRealmModel
FROM questionnaire_assignments
WHERE range_id = :id
) UNION (
- SELECT eval_id, :user_id, '-2', :timestamp, 0
- FROM eval_range
- WHERE range_id = :id
- ) UNION (
SELECT `news_id`, :user_id, `pluginid`, :timestamp, 0
FROM `news_range`
JOIN `plugins` ON (`pluginclassname` = 'CoreOverview')
@@ -786,7 +765,7 @@ class MyRealmModel
foreach ($sem_courses as $sem_key => $collection) {
$_tmp_courses[$sem_key] = [];
foreach ($collection as $course) {
- $modules = Course::getMVVModulesForCourseId($course['seminar_id']);
+ $modules = Course::getMVVModulesForCourseId($course['seminar_id'], ['genehmigt']);
if ($modules) {
$modules = array_map(function (Modul $module) {
return $module->getDisplayName();
diff --git a/lib/classes/NotificationCenter.class.php b/lib/classes/NotificationCenter.php
index aaaa9e8..178fd62 100644
--- a/lib/classes/NotificationCenter.class.php
+++ b/lib/classes/NotificationCenter.php
@@ -1,7 +1,7 @@
<?php
# Lifter010: TODO
/*
- * NotificationCenter.class.php - NotificationCenter class
+ * NotificationCenter.php - NotificationCenter class
*
* Copyright (c) 2009 Elmar Ludwig
*
diff --git a/lib/classes/OAuth1.php b/lib/classes/OAuth1.php
new file mode 100644
index 0000000..1695f9f
--- /dev/null
+++ b/lib/classes/OAuth1.php
@@ -0,0 +1,167 @@
+<?php
+namespace Studip;
+
+use Psr\Http\Message\ServerRequestInterface as Request;
+use RuntimeException;
+
+/**
+ * Basic oauth1 request handling for Stud.IP using PSR-7 http messages.
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @license GPL2 or any later version
+ * @since Stud.IP 6.0
+ */
+final class OAuth1
+{
+ /**
+ * Signs a given request.
+ *
+ * @throws RuntimeException if a request for any other oauth version then
+ * 1.0 shall be signed
+ */
+ public static function signRequest(
+ Request $request,
+ string $consumerSecret,
+ string $tokenSecret,
+ string $method
+ ): string {
+ if (
+ isset($request->getQueryParams()['oauth_version'])
+ && $request->getQueryParams()['oauth_version'] !== '1.0'
+ ) {
+ throw new RuntimeException(self::class . ' only supports OAuth 1.0 requests');
+ }
+
+ return self::hash(
+ $method,
+ self::getSignatureBaseString($request),
+ self::urlencode($consumerSecret) . '&' . self::urlencode($tokenSecret)
+ );
+ }
+
+ /**
+ * Verifies an oauth request.
+ *
+ * @throws RuntimeException if any necessary oauth parameter is missing
+ */
+ public static function verifyRequest(
+ Request $request,
+ string $consumerSecret,
+ string $tokenSecret
+ ): bool {
+ $parameters = self::extractParameters($request);
+
+ $required = [
+ 'oauth_consumer_key',
+ 'oauth_nonce',
+ 'oauth_signature',
+ 'oauth_signature_method',
+ 'oauth_timestamp',
+ ];
+
+ $missing = array_diff($required, array_keys($parameters));
+ if (count($missing) > 0) {
+ throw new RuntimeException('Missing oauth parameters ' . implode(', ', $missing));
+ }
+
+ $signatureToVerify = $parameters['oauth_signature'];
+ unset($parameters['oauth_signature']);
+
+ $signature = self::signRequest(
+ $request->withQueryParams($parameters),
+ $consumerSecret,
+ $tokenSecret,
+ $parameters['oauth_signature_method']
+ );
+
+ return $signature === $signatureToVerify;
+ }
+
+ /**
+ * Extracts the oauth parameters either from the Authorization header or
+ * from the query string.
+ */
+ public static function extractParameters(Request $request): array
+ {
+ $parameters = $request->getQueryParams();
+
+ $header = $request->getHeaderLine('Authorization');
+ if ($header && str_starts_with($header, 'OAuth ')) {
+ $temp = substr($header, 6);
+ $chunks = explode(',', $temp);
+
+ foreach ($chunks as $chunk) {
+ [$key, $value] = explode('=', $chunk, 2);
+ $value = trim($value, '"');
+ $parameters[$key] = self::urldecode($value);
+ }
+ }
+
+ return $parameters;
+ }
+
+ /**
+ * Creates the base string for the signature. It consists of:
+ *
+ * - The uppercase request method
+ * - The request URL
+ * - the sorted and urlencoded parameters of the request
+ *
+ * The urlencoded parts are concatenated together into a single string
+ * separated by the '&' character.
+ *
+ *
+ */
+ public static function getSignatureBaseString(Request $request): string
+ {
+ $parameters = $request->getQueryParams();
+ ksort($parameters);
+
+ return implode('&', array_map(
+ self::urlencode(...),
+ [
+ strtoupper($request->getMethod()),
+ (string) $request->getUri()->withQuery(''),
+ http_build_query($parameters, '', '&', PHP_QUERY_RFC3986),
+ ]
+ ));
+ }
+
+ /**
+ * Hashes a given text with a given key by the given method.
+ *
+ * @throws RuntimeException if the given hash method is not supported
+ */
+ public static function hash(string $method, string $text, string $key): string
+ {
+ $method = strtolower($method);
+ return match ($method) {
+ 'hmac-sha1', 'sha1' => base64_encode(hash_hmac('sha1', $text, $key, true)),
+ 'hmac-sha256', 'sha256' => base64_encode(hash_hmac('sha256', $text, $key, true)),
+ 'hmac-sha512', 'sha512' => base64_encode(hash_hmac('sha512', $text, $key, true)),
+
+ 'plaintext' => $key,
+
+ default => throw new RuntimeException('Unsupported sigature method "' . $method . '"'),
+ };
+ }
+
+ /**
+ * Urlencodes a given input
+ */
+ public static function urldecode(string $input): string
+ {
+ return rawurldecode($input);
+ }
+
+ /**
+ * Urldecodes a given input
+ */
+ public static function urlencode(string $input): string
+ {
+ $encoded = rawurlencode($input);
+ return str_starts_with($encoded, '/%7E')
+ ? str_replace('/%7E', '/~', $encoded)
+ : $encoded;
+ }
+}
diff --git a/lib/classes/OAuth2/NegotiatesWithPsr7.php b/lib/classes/OAuth2/NegotiatesWithPsr7.php
index 0edf243..b2ee5a1 100644
--- a/lib/classes/OAuth2/NegotiatesWithPsr7.php
+++ b/lib/classes/OAuth2/NegotiatesWithPsr7.php
@@ -5,7 +5,7 @@ namespace Studip\OAuth2;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Psr7\Response;
-use Trails_Response;
+use Trails\Response as TrailsResponse;
trait NegotiatesWithPsr7
{
@@ -19,9 +19,9 @@ trait NegotiatesWithPsr7
return new Response();
}
- protected function convertPsrResponse(ResponseInterface $response): Trails_Response
+ protected function convertPsrResponse(ResponseInterface $response): TrailsResponse
{
- $trailsResponse = new Trails_Response((string) $response->getBody(), [], $response->getStatusCode());
+ $trailsResponse = new TrailsResponse((string) $response->getBody(), [], $response->getStatusCode());
foreach ($response->getHeaders() as $key => $values) {
foreach ($values as $value) {
$trailsResponse->add_header($key, $value);
diff --git a/lib/classes/OpenGraph.php b/lib/classes/OpenGraph.php
index 75326aa..845f5cb 100644
--- a/lib/classes/OpenGraph.php
+++ b/lib/classes/OpenGraph.php
@@ -11,34 +11,71 @@ class OpenGraph
/**
* Extracts urls and their according open graph infos from a given string
*
- * @param String $string Text to extract urls and open graph infos from
+ * @param string|null $string Text to extract urls and open graph infos from
* @return OpenGraphURLCollection containing the extracted urls
*/
- public static function extract($string)
+ public static function extract(?string $string): OpenGraphURLCollection
{
- $collection = new OpenGraphURLCollection;
-
- if (Config::get()->OPENGRAPH_ENABLE) {
- $regexp = StudipCoreFormat::getStudipMarkup('links')['start'];
- $matched = preg_match_all('/' . $regexp . '/ums', $string, $matches, PREG_SET_ORDER);
- foreach ($matches as $match) {
- $url = $match[2];
-
- if (!$url) {
- continue;
- }
-
- if (!isLinkIntern($url)) {
- $og_url = OpenGraphURL::fromURL($url);
- if ($og_url && !$collection->find($og_url->id)) {
- $og_url->store();
-
- $collection[] = $og_url;
- }
- }
+ $collection = new OpenGraphURLCollection();
+
+ if (!Config::get()->OPENGRAPH_ENABLE || !$string) {
+ return $collection;
+ }
+
+ if (Studip\Markup::isHtml($string)) {
+ $urls = self::extractUrlsFromHtml($string);
+ } else {
+ $urls = self::extractUrlsFromText($string);
+ }
+
+ foreach ($urls as $url) {
+ $og_url = OpenGraphURL::fromURL($url);
+ if ($og_url && !$collection->find($og_url->id)) {
+ $og_url->store();
+
+ $collection[] = $og_url;
}
}
return $collection;
}
+
+ public static function filterURLs(array $urls): array
+ {
+ return array_filter($urls, function (string $url): bool {
+ if (!$url) {
+ return false;
+ }
+
+ return !isLinkIntern($url);
+ });
+ }
+
+ public static function extractUrlsFromText(string $text): array
+ {
+ $regexp = StudipCoreFormat::getStudipMarkup('links')['start'];
+ preg_match_all('/' . $regexp . '/ums', $text, $matches, PREG_SET_ORDER);
+ $urls = array_column($matches, 2);
+
+ return self::filterURLs($urls);
+ }
+
+ public static function extractUrlsFromHtml(string $html): array
+ {
+ $document = new DOMDocument();
+ $document->loadHTML($html, LIBXML_NOWARNING | LIBXML_NOERROR);
+
+ $elements = $document->getElementsByTagName('a');
+
+ $urls = [];
+ foreach ($elements as $element) {
+ if (!$element->hasAttribute('href')) {
+ continue;
+ }
+
+ $urls[] = $element->getAttribute('href');
+ }
+
+ return self::filterURLs($urls);
+ }
}
diff --git a/lib/classes/PageLayout.php b/lib/classes/PageLayout.php
index fca69b5..7f95b7a 100644
--- a/lib/classes/PageLayout.php
+++ b/lib/classes/PageLayout.php
@@ -36,6 +36,11 @@ class PageLayout
private static $help_keyword;
/**
+ * current help URL (or null if unset)
+ */
+ private static ?string $help_url = null;
+
+ /**
* base item path for tab view (defaults to active item in top nav)
*/
private static $tab_navigation_path = false;
@@ -134,6 +139,21 @@ class PageLayout
self::addScript('studip-wysiwyg.js?v=' . $v);
self::addStylesheet('print.css?v=' . $v, ['media' => 'print']);
+
+ if (Studip\Debug\DebugBar::isActivated()) {
+ $old_base = URLHelper::setBaseURL($GLOBALS['ABSOLUTE_URI_STUDIP']);
+
+ self::addHeadElement('link', [
+ 'href' => URLHelper::getURL('dispatch.php/debugbar/css'),
+ 'rel' => 'stylesheet',
+ 'type' => 'text/css',
+ ]);
+ self::addHeadElement('script', [
+ 'src' => URLHelper::getURL('dispatch.php/debugbar/js'),
+ ], '');
+
+ URLHelper::setBaseURL($old_base);
+ }
}
/**
@@ -179,7 +199,24 @@ class PageLayout
*/
public static function getHelpKeyword()
{
- return isset(self::$help_keyword) ? self::$help_keyword : 'Basis.Allgemeines';
+ return self::$help_keyword ?? 'Basis.Allgemeines';
+ }
+
+ /**
+ * Set the help URL in the help bar. Pass null to fall back to the help keyword.
+ */
+ public static function setHelpUrl(?string $url): void
+ {
+ self::$help_url = $url;
+ }
+
+ /**
+ * Get the current help URL. If no URL is set explicitly, the URL for
+ * the help keyword is used.
+ */
+ public static function getHelpUrl(): ?string
+ {
+ return self::$help_url ?? format_help_url(self::getHelpKeyword());
}
/**
@@ -562,10 +599,18 @@ class PageLayout
if (!isset($_SESSION['messages'])) {
$_SESSION['messages'] = [];
}
+
+ $structure = [
+ 'type' => $message->class,
+ 'message' => $message->message,
+ 'details' => $message->details,
+ 'closeable' => $message->isCloseable()
+ ];
+
if ($id === null ) {
- $_SESSION['messages'][] = $message;
+ $_SESSION['messages'][] = $structure;
} else {
- $_SESSION['messages'][$id] = $message;
+ $_SESSION['messages'][$id] = $structure;
}
}
diff --git a/lib/classes/PluginAdministration.php b/lib/classes/PluginAdministration.php
index 2732f58..faba3ec 100644
--- a/lib/classes/PluginAdministration.php
+++ b/lib/classes/PluginAdministration.php
@@ -440,27 +440,26 @@ class PluginAdministration
// get plugin meta data
$pluginclass = $manifest['pluginclassname'];
$origin = $manifest['origin'];
- $min_version = $manifest['studipMinVersion'];
- $max_version = $manifest['studipMaxVersion'];
+ $min_version = $manifest['studipMinVersion'] ?? null;
+ $max_version = $manifest['studipMaxVersion'] ?? null;
// check for compatible version
- if ((isset($min_version) && StudipVersion::olderThan($min_version)) ||
- (isset($max_version) && StudipVersion::newerThan($max_version))) {
+ if (
+ (isset($min_version) && StudipVersion::olderThan($min_version))
+ || (isset($max_version) && StudipVersion::newerThan($max_version))
+ ) {
throw new PluginInstallationException(_('Das Plugin ist mit dieser Stud.IP-Version nicht kompatibel.'));
}
// determine the plugin path
- $basepath = Config::get()->PLUGINS_PATH;
$pluginpath = $origin . '/' . $pluginclass;
- $pluginregistered = $plugin_manager->getPluginInfo($pluginclass);
-
- if ($pluginregistered) {
- new PluginInstallationException(_('Das Plugin ist bereits registriert.'));
- }
-
// create database schema if needed
- $this->createDBSchema($plugindir, $manifest, $pluginregistered);
+ $this->createDBSchema(
+ $plugindir,
+ $manifest,
+ (bool) $plugin_manager->getPluginInfo($pluginclass)
+ );
// now register the plugin in the database
$pluginid = $plugin_manager->registerPlugin($manifest['pluginname'], $pluginclass, $pluginpath);
diff --git a/lib/classes/PluginController.php b/lib/classes/PluginController.php
new file mode 100644
index 0000000..d57a90d
--- /dev/null
+++ b/lib/classes/PluginController.php
@@ -0,0 +1,72 @@
+<?php
+/**
+ * Copyright (c) 2014 Rasmus Fuhse <fuhse@data-quest.de>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ */
+
+class PluginController extends StudipController
+{
+ public function __construct($dispatcher)
+ {
+ parent::__construct($dispatcher);
+
+ if (!isset($dispatcher->current_plugin)) {
+ throw new Exception('Plugin missing for plugin controller!');
+ }
+ $this->plugin = $dispatcher->current_plugin;
+
+ if ($this->plugin && $this->plugin->hasTranslation()) {
+ // Localization
+ $this->_ = function ($string) {
+ return call_user_func_array(
+ [$this->plugin, '_'],
+ func_get_args()
+ );
+ };
+
+ $this->_n = function ($string0, $tring1, $n) {
+ return call_user_func_array(
+ [$this->plugin, '_n'],
+ func_get_args()
+ );
+ };
+ }
+ }
+
+ /**
+ * Creates the body element id for this controller a given action.
+ *
+ * @param string $unconsumed_path Unconsumed path to extract action from
+ * @return string
+ */
+ protected function getBodyElementIdForControllerAndAction($unconsumed_path)
+ {
+ $body_id = implode('-', [
+ 'plugin',
+ strtosnakecase(get_class($this->plugin)),
+ parent::getBodyElementIdForControllerAndAction($unconsumed_path),
+ ]);
+
+ return $body_id;
+ }
+
+ /**
+ * Intercepts all non-resolvable method calls in order to correctly handle
+ * calls to _ and _n.
+ *
+ * @param string $method
+ * @param array $arguments
+ * @return mixed
+ */
+ public function __call($method, $arguments)
+ {
+ if (isset($this->_template_variables[$method]) && is_callable($this->_template_variables[$method])) {
+ return call_user_func_array($this->_template_variables[$method], $arguments);
+ }
+ return parent::__call($method, $arguments);
+ }
+}
diff --git a/lib/classes/Privacy.php b/lib/classes/Privacy.php
index 38e6e80..b38838c 100644
--- a/lib/classes/Privacy.php
+++ b/lib/classes/Privacy.php
@@ -59,7 +59,6 @@ class Privacy
Courseware\UserProgress::class,
],
'quest' => [
- Evaluation::class,
Questionnaire::class,
QuestionnaireAnswer::class,
QuestionnaireAnonymousAnswer::class,
@@ -115,7 +114,7 @@ class Privacy
}
if (!$section || $section === 'plugins') {
- foreach (PluginEngine::getPlugins('PrivacyPlugin') as $plugin) {
+ foreach (PluginEngine::getPlugins(PrivacyPlugin::class) as $plugin) {
$plugin->exportUserData($storage);
}
}
diff --git a/lib/classes/PrivacyObject.interface.php b/lib/classes/PrivacyObject.php
index 8129634..8129634 100644
--- a/lib/classes/PrivacyObject.interface.php
+++ b/lib/classes/PrivacyObject.php
diff --git a/lib/classes/ProfileModel.php b/lib/classes/ProfileModel.php
deleted file mode 100644
index 6827ba6..0000000
--- a/lib/classes/ProfileModel.php
+++ /dev/null
@@ -1,213 +0,0 @@
-<?php
-/**
- * ProfileModel
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author David Siegfried <david.siegfried@uni-oldenburg.de>
- * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category Stud.IP
- * @since 2.4
- */
-
-class ProfileModel
-{
- protected $perm;
- /**
- * Internal current selected user id
- * @var String
- */
- protected $current_user;
-
- /**
- * Internal current logged in user id
- * @var String
- */
- protected $user;
-
- /**
- * Internal user homepage visbilities
- * @var array
- */
- protected $visibilities;
-
- /**
- * Get informations on depending selected user
- * @param String $current_user
- * @param String $user
- */
- public function __construct($current_user, $user)
- {
- $this->current_user = User::find($current_user);
- $this->user = User::find($user);
- $this->visibilities = $this->getHomepageVisibilities();
- $this->perm = $GLOBALS['perm'];
- }
-
- /**
- * Get the homepagevisibilities
- *
- * @return array
- */
- public function getHomepageVisibilities()
- {
- $visibilities = get_local_visibility_by_id(
- $this->current_user ? $this->current_user->id : null,
- 'homepage'
- );
- if (is_array(json_decode($visibilities, true))) {
- return json_decode($visibilities, true);
- }
- return [];
- }
-
- /**
- * Returns the visibility value
- *
- * @return String
- */
- public function getVisibilityValue($param, $visibility = '')
- {
- if (Visibility::verify($visibility ?: $param, $this->current_user->user_id)) {
- return $this->current_user->$param;
- }
- return false;
- }
-
- /**
- * Returns a specific value of the visibilies
- * @param String $param
- * @return String
- */
-
- public function getSpecificVisibilityValue($param)
- {
- if (!empty($this->visibilities[$param])) {
- return $this->visibilities[$param];
- }
- return false;
- }
-
- /**
- * Creates an array with all seminars
- *
- * @return array
- */
- public function getDozentSeminars()
- {
- $courses = [];
- $semester = [];
- $next_semester = Semester::findNext();
- $current_semester = Semester::findCurrent();
- $previous_semester = Semester::findPrevious();
- if ($next_semester) {
- $semester[$next_semester->id] = $next_semester;
- }
- if ($current_semester) {
- $semester[$current_semester->id] = $current_semester;
- }
- if ($previous_semester) {
- $semester[$previous_semester->id] = $previous_semester;
- }
- $field = 'name';
- if (Config::get()->IMPORTANT_SEMNUMBER) {
- $field = "veranstaltungsnummer,{$field}";
- }
- $allcourses = new SimpleCollection(Course::findBySQL("INNER JOIN seminar_user USING(Seminar_id) WHERE user_id=? AND seminar_user.status='dozent' AND seminare.visible=1", [$this->current_user->id]));
- foreach (array_filter($semester) as $one) {
- $courses[(string) $one->name] = $allcourses->filter(function ($c) use ($one) {
- if (Config::get()->HIDE_STUDYGROUPS_FROM_PROFILE && $c->isStudygroup()) {
- return false;
- }
- if (!$c->isOpenEnded()) {
- return $c->isInSemester($one);
- } elseif ($one->isCurrent()) {
- return $c;
- }
- return false;
- })->orderBy($field);
-
- if (!$courses[(string) $one->name]->count()) {
- unset($courses[(string) $one->name]);
- }
- }
- return $courses;
- }
-
- /**
- * Collect user datafield informations
- *
- * @return array
- */
- public function getDatafields()
- {
- // generische Datenfelder aufsammeln
- $short_datafields = [];
- $long_datafields = [];
- foreach (DataFieldEntry::getDataFieldEntries($this->current_user->user_id, 'user') as $entry) {
- if ($entry->isVisible() && $entry->getDisplayValue()
- && Visibility::verify($entry->getID(), $this->current_user->user_id))
- {
- if ($entry instanceof DataFieldTextareaEntry) {
- $long_datafields[] = $entry;
- } else {
- $short_datafields[] = $entry;
- }
- }
- }
-
- return [
- 'long' => $long_datafields,
- 'short' => $short_datafields,
- ];
- }
-
- /**
- * Filter long datafiels from the datafields
- *
- * @return array
- */
- public function getLongDatafields()
- {
- $datafields = $this->getDatafields();
- $array = [];
-
- if (empty($datafields)) {
- return null;
- }
- foreach ($datafields['long'] as $entry) {
- $array[(string) $entry->getName()] = [
- 'content' => $entry->getDisplayValue(),
- 'visible' => '(' . $entry->getPermsDescription() . ')',
- ];
- }
-
- return $array;
- }
-
- /**
- * Filter short datafiels from the datafields
- *
- * @return array
- */
- public function getShortDatafields()
- {
- $shortDatafields = $this->getDatafields();
- $array = [];
-
- if (empty($shortDatafields)) {
- return null;
- }
-
- foreach ($shortDatafields['short'] as $entry) {
- $array[(string) $entry->getName()] = [
- 'content' => $entry->getDisplayValue(),
- 'visible' => '(' . $entry->getPermsDescription() . ')',
- ];
- }
- return $array;
- }
-}
diff --git a/lib/classes/QuestionType.interface.php b/lib/classes/QuestionType.php
index ada6005..03019fe 100644
--- a/lib/classes/QuestionType.interface.php
+++ b/lib/classes/QuestionType.php
@@ -60,7 +60,8 @@ interface QuestionType {
*
* Try to prefix all your input variables at least with the id of the question,
* so that they will never conflict with other variables.
- * @return Flexi_Template
+ *
+ * @return Flexi\Template
*/
public function getDisplayTemplate();
@@ -82,11 +83,13 @@ interface QuestionType {
/**
* Returns a template with the results of this question.
+ *
* @param $only_user_ids : array\null array of user_ids that the results should be restricted to.
* this is used to show only a subset of results to the user for
* visible evaluation of the results. If the questionnaire is anonymous
* just do nothing.
- * @return Flexi_Template
+ *
+ * @return Flexi\Template
*/
public function getResultTemplate($only_user_ids = null);
diff --git a/lib/classes/QuickSearch.class.php b/lib/classes/QuickSearch.php
index d3983a3..f3e18cc 100644
--- a/lib/classes/QuickSearch.class.php
+++ b/lib/classes/QuickSearch.php
@@ -1,7 +1,7 @@
<?php
# Lifter010: TODO
/**
- * QuickSearch.class.php - GUI class for quciksearch
+ * QuickSearch.php - GUI class for quciksearch
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
@@ -66,7 +66,7 @@
* $searcher = new TeacherSearch();
* print QuickSearch::get("username", $searcher)->withButton->render();
* //code-end
- * Watch the SearchType class in lib/classes/searchtypes/SearchType.class.php
+ * Watch the SearchType class in lib/classes/searchtypes/SearchType.php
* for details.
* Enjoy!
*/
@@ -166,7 +166,7 @@ class QuickSearch
/**
* constructor which prepares a searchfield for persons, courses, institutes or
* special items you may want to search for. This is a GUI-class, see
- * QuickSearch.class.php for further documentation.
+ * QuickSearch.php for further documentation.
*
* @param string $name the name of the destinated variable in your html-form. Handle it
* as if it was an '<input type="text" name="yourname">' input.
diff --git a/lib/classes/Range.interface.php b/lib/classes/Range.php
index 97aa074..97aa074 100644
--- a/lib/classes/Range.interface.php
+++ b/lib/classes/Range.php
diff --git a/lib/classes/RangeConfig.class.php b/lib/classes/RangeConfig.php
index b1030e3..ad48207 100644
--- a/lib/classes/RangeConfig.class.php
+++ b/lib/classes/RangeConfig.php
@@ -1,6 +1,6 @@
<?php
/**
- * RangeConfig.class.php
+ * RangeConfig.php
* provides access to object preferences
*
* This program is free software; you can redistribute it and/or
diff --git a/lib/classes/RangeTreeObject.class.php b/lib/classes/RangeTreeObject.php
index 579db43..06f84bf 100644
--- a/lib/classes/RangeTreeObject.class.php
+++ b/lib/classes/RangeTreeObject.php
@@ -5,7 +5,7 @@
# Lifter010: TODO
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// RangeTreeObject.class.php
+// RangeTreeObject.php
// Class to handle items in the "range tree"
//
// Copyright (c) 2002 André Noack <noack@data-quest.de>
diff --git a/lib/classes/RangeTreeObjectFak.class.php b/lib/classes/RangeTreeObjectFak.php
index 76cc366..9e980ca 100644
--- a/lib/classes/RangeTreeObjectFak.class.php
+++ b/lib/classes/RangeTreeObjectFak.php
@@ -5,7 +5,7 @@
# Lifter010: TODO
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// RangeTreeObjectFak.class.php
+// RangeTreeObjectFak.php
// Class to handle items in the "range tree"
//
// Copyright (c) 2002 André Noack <noack@data-quest.de>
diff --git a/lib/classes/RangeTreeObjectInst.class.php b/lib/classes/RangeTreeObjectInst.php
index c291dfc..d41b03d 100644
--- a/lib/classes/RangeTreeObjectInst.class.php
+++ b/lib/classes/RangeTreeObjectInst.php
@@ -5,7 +5,7 @@
# Lifter010: TODO
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// RangeTreeObjectInst.class.php
+// RangeTreeObjectInst.php
// Class to handle items in the "range tree"
//
// Copyright (c) 2002 André Noack <noack@data-quest.de>
diff --git a/lib/classes/Request.class.php b/lib/classes/Request.php
index 50ab8c4..d72a6a9 100644
--- a/lib/classes/Request.class.php
+++ b/lib/classes/Request.php
@@ -45,55 +45,40 @@ class Request implements ArrayAccess, IteratorAggregate
/**
* ArrayAccess: Check whether the given offset exists.
- *
- * @todo Add bool return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetExists($offset)
+ public function offsetExists($offset): bool
{
return isset($this->params[$offset]);
}
/**
* ArrayAccess: Get the value at the given offset.
- *
- * @todo Add mixed return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetGet($offset)
+ public function offsetGet($offset): mixed
{
return $this->params[$offset] ?? null;
}
/**
* ArrayAccess: Set the value at the given offset.
- *
- * @todo Add void return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetSet($offset, $value)
+ public function offsetSet($offset, $value): void
{
$this->params[$offset] = $value;
}
/**
* ArrayAccess: Delete the value at the given offset.
- *
- * @todo Add void return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetUnset($offset)
+ public function offsetUnset($offset): void
{
unset($this->params[$offset]);
}
/**
- * IteratorAggregate: Create interator for request parameters.
- *
- * @todo Add Traversable return type when Stud.IP requires PHP8 minimal
+ * IteratorAggregate: Create iterator for request parameters.
*/
- #[ReturnTypeWillChange]
- public function getIterator()
+ public function getIterator(): Traversable
{
return new ArrayIterator((array)$this->params);
}
@@ -363,7 +348,7 @@ class Request implements ArrayAccess, IteratorAggregate
//The second format and value is only added
//when $second_param and $second_format are set
//and a second value could be retrieved.
- if ($second_param and $second_format) {
+ if ($second_param && $second_format) {
$second_value = Request::get($second_param);
if ($second_value) {
$combined_format .= ' ' . $second_format;
@@ -379,11 +364,22 @@ class Request implements ArrayAccess, IteratorAggregate
//Now we return a DateTime object created from the
//specified date value(s):
- return DateTime::createFromFormat(
+ $result = DateTime::createFromFormat(
$combined_format,
$combined_value,
$time_zone
);
+
+ if (
+ $result
+ && !$second_param
+ && !$second_format
+ && !preg_match('/[aAghGHisvu]/', $format) // Ensure no time in format
+ ) {
+ $result->setTime(0, 0);
+ }
+
+ return $result;
}
diff --git a/lib/classes/ResetButton.class.php b/lib/classes/ResetButton.php
index 8702c4b..8702c4b 100644
--- a/lib/classes/ResetButton.class.php
+++ b/lib/classes/ResetButton.php
diff --git a/lib/classes/ResponsiveHelper.php b/lib/classes/ResponsiveHelper.php
index 3e29db3..f02ee80 100644
--- a/lib/classes/ResponsiveHelper.php
+++ b/lib/classes/ResponsiveHelper.php
@@ -210,30 +210,29 @@ class ResponsiveHelper
$courses = [];
}
- $items = [];
-
- $standardIcon = Icon::create('seminar', Icon::ROLE_INFO_ALT)->asImagePath();
-
// Add current course to list.
if (Context::get()) {
$courses[] = Context::get();
}
- foreach ($courses as $course) {
+ if (Context::isInstitute()) {
+ $avatarClass = InstituteAvatar::class;
+ $url = 'dispatch.php/institute/overview';
+ $standardIcon = Icon::create('institute', Icon::ROLE_INFO_ALT)->asImagePath();
+ } else {
$avatarClass = CourseAvatar::class;
$url = 'dispatch.php/course/details';
- if (Context::isInstitute()) {
- $avatarClass = InstituteAvatar::class;
- $url = 'dispatch.php/institute/overview';
- $standardIcon = Icon::create('institute', Icon::ROLE_INFO_ALT)->asImagePath();
- }
+ $standardIcon = Icon::create('seminar', Icon::ROLE_INFO_ALT)->asImagePath();
+ }
+ $items = [];
+ foreach ($courses as $course) {
$avatar = $avatarClass::getAvatar($course->id);
$hasAvatar = $avatar->is_customized();
$icon = $hasAvatar ? $avatar->getURL(Avatar::SMALL) : $standardIcon;
- $cnav = [
+ $items['browse/my_courses/' . $course->id] = [
'icon' => $icon,
'avatar' => $hasAvatar,
'title' => $course->getFullName(),
@@ -242,60 +241,60 @@ class ResponsiveHelper
'path' => 'browse/my_courses/' . $course->id,
'visible' => true,
'active' => Context::getId() === $course->id,
- 'children' => []
+ 'children' => self::getRangeNavigation(
+ $course,
+ 'browse/my_courses/' . $course->id,
+ $activated
+ ),
];
- $path = 'browse/my_courses/' . $course->id;
-
- foreach ($course->tools as $tool) {
- if (Seminar_Perm::get()->have_studip_perm($tool->getVisibilityPermission(), $course->id)) {
-
- $studip_module = $tool->getStudipModule();
- if ($studip_module instanceof StudipModule) {
- $tool_nav = $studip_module->getTabNavigation($course->id) ?: [];
- foreach ($tool_nav as $nav_name => $navigation) {
- if ($nav_name && is_a($navigation, 'Navigation')) {
- if (!empty($tool->metadata['displayname'])) {
- $navigation->setTitle($tool->getDisplayname());
- }
- $cnav['children'][$path . '/' . $nav_name] = [
- 'icon' => $navigation->getImage() ? $navigation->getImage()->asImagePath() : '',
- 'title' => $navigation->getTitle(),
- 'url' => URLHelper::getURL($navigation->getURL(), ['cid' => $course->id]),
- 'parent' => 'browse/my_courses/' . $course->id,
- 'path' => 'browse/my_courses/' . $course->id . '/' . $nav_name,
- 'visible' => true,
- 'active' => $navigation->isActive(),
- 'children' => static::getChildren(
- $navigation,
- 'browse/my_courses/' . $course->id . '/' . $nav_name,
- $activated,
- $course->id
- ),
- ];
- }
- }
- }
- }
- }
+ }
- if ($GLOBALS['perm']->have_studip_perm('tutor', $course->id)) {
- $cnav['children'][$path . '/plus'] = [
- 'icon' => Icon::create('add', Icon::ROLE_INFO_ALT)->asImagePath(),
- 'title' => _('Mehr...'),
- 'url' => URLHelper::getURL('dispatch.php/course/plus/index', ['cid' => $course->id]),
- 'parent' => 'browse/my_courses/' . $course->id,
- 'path' => 'browse/my_courses/' . $course->id . '/plus/index',
- 'visible' => true,
- 'active' => false,
- 'children' => [],
- ];
- }
+ return $items;
+ }
+
+ private static function getRangeNavigation(Range $range, string $path_prefix, array &$activated): array
+ {
+ if ($range->id === Context::getId()) {
+ $navigation = Navigation::getItem('/course');
+ } else {
+ $navigation = new CourseNavigation($range);
+ }
- $items['browse/my_courses/' . $course->id] = $cnav;
+ $result = [];
+ foreach ($navigation as $nav_name => $nav) {
+ $result[$path_prefix . '/' . $nav_name] = [
+ 'icon' => $nav->getImage() ? $nav->getImage()->asImagePath() : '',
+ 'title' => $nav->getTitle(),
+ 'url' => URLHelper::getURL($nav->getURL(), ['cid' => $range->id]),
+ 'parent' => 'browse/my_courses/' . $range->id,
+ 'path' => 'browse/my_courses/' . $range->id . '/' . $nav_name,
+ 'visible' => true,
+ 'active' => $nav->isActive(),
+ 'children' => static::getChildren(
+ $nav,
+ 'browse/my_courses/' . $range->id . '/' . $nav_name,
+ $activated,
+ $range->id
+ ),
+ ];
}
- return $items;
+ // Move admin page to the end
+ if (count($result) > 0) {
+ $first_path = array_keys($result)[0];
+ if (str_ends_with($first_path, '/admin')) {
+ $admin_navigation = array_slice(array_values($result), 0, 1)[0];
+ $admin_navigation['title'] = _('Verwaltung');
+ $admin_navigation['icon'] = Icon::create('add', Icon::ROLE_INFO_ALT)->asImagePath();
+ $result = array_merge(
+ array_slice($result, 1),
+ [$path_prefix . '/admin' => $admin_navigation]
+ );
+ }
+ }
+
+ return $result;
}
}
diff --git a/lib/classes/SQLQuery.php b/lib/classes/SQLQuery.php
index 0442604..6c8f49e 100644
--- a/lib/classes/SQLQuery.php
+++ b/lib/classes/SQLQuery.php
@@ -215,14 +215,22 @@ class SQLQuery
/**
* Fetches the whole resultset as an array of associative arrays. If you define
* a sorm_class the result will be an array of the sorm-objects.
- * @param $sorm_class_or_column : column name, a class of SimpleORMap or null for associative array.
- * @return array of arrays or array of objects or array of values.
+ *
+ * @template T of SimpleORMap
+ * @param class-string<T>|string|null $sorm_class_or_column : column name, a class of SimpleORMap or null for associative array.
+ * @param int|null $max_results Maximum number of results to return
+ * @return array[]|T[]|mixed[] arrays or array of objects or array of values.
+ *
+ * @throws OverflowException if number of found rows is greater than $max_results
*/
- public function fetchAll($sorm_class_or_column = null)
+ public function fetchAll($sorm_class_or_column = null, ?int $max_results = null)
{
NotificationCenter::postNotification('SQLQueryWillExecute', $this);
- if (is_string($sorm_class_or_column) && !is_subclass_of($sorm_class_or_column, "SimpleORMap")) {
+ if (
+ is_string($sorm_class_or_column)
+ && !is_subclass_of($sorm_class_or_column, SimpleORMap::class)
+ ) {
$sql = "SELECT `{$this->settings['table']}`.`{$sorm_class_or_column}` ";
} else {
$sql = "SELECT `{$this->settings['table']}`.* ";
@@ -239,16 +247,31 @@ class SQLQuery
NotificationCenter::postNotification('SQLQueryDidExecute', $this);
- if (is_string($sorm_class_or_column) && !is_subclass_of($sorm_class_or_column, "SimpleORMap")) {
- return $statement->fetchAll(PDO::FETCH_COLUMN, 0);
+ if (
+ is_string($sorm_class_or_column)
+ && !is_subclass_of($sorm_class_or_column, SimpleORMap::class)
+ ) {
+ return $statement->fetchAll(PDO::FETCH_COLUMN);
}
- $alldata = $statement->fetchAll(PDO::FETCH_ASSOC);
- if (!$sorm_class_or_column) {
- return $alldata;
+ $statement->setFetchMode(PDO::FETCH_ASSOC);
+
+ $result = [];
+ $count = 0;
+ foreach ($statement as $row) {
+ $result[$count++] = $sorm_class_or_column ? $sorm_class_or_column::buildExisting($row) : $row;
+
+ if ($max_results && $count > $max_results) {
+ // Count remaining rows
+ $statement->setFetchMode(PDO::FETCH_COLUMN, 0);
+ while ($statement->fetch()) {
+ $count += 1;
+ }
+ throw new OverflowException($count);
+ }
}
- return array_map("{$sorm_class_or_column}::buildExisting", $alldata);
+ return $result;
}
/**
diff --git a/lib/classes/Score.class.php b/lib/classes/Score.php
index 8f038f9..3b7f149 100644
--- a/lib/classes/Score.class.php
+++ b/lib/classes/Score.php
@@ -1,6 +1,6 @@
<?
/**
- * Score.class.php - Score class
+ * Score.php - Score class
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
@@ -121,7 +121,7 @@ class Score
public static function GetMyScore($user_or_id = null)
{
$user = $user_or_id ? User::toObject($user_or_id) : User::findCurrent();
- $cache = StudipCacheFactory::getCache();
+ $cache = \Studip\Cache\Factory::getCache();
if ($cache->read("user_score_of_".$user->id)) {
return $cache->read("user_score_of_".$user->id);
}
@@ -212,7 +212,7 @@ class Score
'date_column' => 'chdate'
];
- foreach (PluginManager::getInstance()->getPlugins('ScorePlugin') as $plugin) {
+ foreach (PluginManager::getInstance()->getPlugins(ScorePlugin::class) as $plugin) {
foreach ((array) $plugin->getPluginActivityTables() as $table) {
if ($table['table']) {
$tables[] = $table;
diff --git a/lib/classes/SemBrowse.class.php b/lib/classes/SemBrowse.php
index 7969a17..536b7e3 100644
--- a/lib/classes/SemBrowse.class.php
+++ b/lib/classes/SemBrowse.php
@@ -484,188 +484,6 @@ class SemBrowse {
ob_end_flush();
}
- public function create_result_xls($headline = '')
- {
- require_once "vendor/write_excel/OLEwriter.php";
- require_once "vendor/write_excel/BIFFwriter.php";
- require_once "vendor/write_excel/Worksheet.php";
- require_once "vendor/write_excel/Workbook.php";
-
- global $SEM_TYPE, $SEM_CLASS, $TMP_PATH;
-
- if (!$headline) {
- $headline = _('Stud.IP Veranstaltungen') . ' - ' . Config::get()->UNI_NAME_CLEAN;
- }
- $tmpfile = null;
- if (is_array($this->sem_browse_data['search_result'])
- && count($this->sem_browse_data['search_result'])) {
- if (!is_object($this->sem_tree)) {
- $the_tree = TreeAbstract::GetInstance('StudipSemTree', false);
- } else {
- $the_tree = $this->sem_tree->tree;
- }
- list($group_by_data, $sem_data) = $this->get_result();
- $tmpfile = $TMP_PATH . '/' . md5(uniqid('write_excel', 1));
- // Creating a workbook
- $workbook = new Workbook($tmpfile);
- $head_format = $workbook->addformat();
- $head_format->set_size(12);
- $head_format->set_bold();
- $head_format->set_align('left');
- $head_format->set_align('vcenter');
-
- $head_format_merged = $workbook->addformat();
- $head_format_merged->set_size(12);
- $head_format_merged->set_bold();
- $head_format_merged->set_align('left');
- $head_format_merged->set_align('vcenter');
- $head_format_merged->set_merge();
- $head_format_merged->set_text_wrap();
-
- $caption_format = $workbook->addformat();
- $caption_format->set_size(10);
- $caption_format->set_align('left');
- $caption_format->set_align('vcenter');
- $caption_format->set_bold();
-
- $data_format = $workbook->addformat();
- $data_format->set_size(10);
- $data_format->set_align('left');
- $data_format->set_align('vcenter');
-
- $caption_format_merged = $workbook->addformat();
- $caption_format_merged->set_size(10);
- $caption_format_merged->set_merge();
- $caption_format_merged->set_align('left');
- $caption_format_merged->set_align('vcenter');
- $caption_format_merged->set_bold();
-
-
- // Creating the first worksheet
- $worksheet1 = $workbook->addworksheet(_('Veranstaltungen'));
- $worksheet1->set_row(0, 20);
- $worksheet1->write_string(0, 0, mb_convert_encoding($headline, 'WINDOWS-1252') ,$head_format);
- $worksheet1->set_row(1, 20);
- $worksheet1->write_string(
- 1,
- 0,
- mb_convert_encoding(sprintf(
- _('%s Veranstaltungen gefunden %s, Gruppierung: %s'),
- count($sem_data),
- $this->sem_browse_data['sset'] ? '(' . _('Suchergebnis') . ')' : '',
- $this->group_by_fields[$this->sem_browse_data['group_by']]['name']
- ), 'WINDOWS-1252'),
- $caption_format
- );
-
- $worksheet1->write_blank(0, 1, $head_format);
- $worksheet1->write_blank(0, 2, $head_format);
- $worksheet1->write_blank(0, 3, $head_format);
-
- $worksheet1->write_blank(1, 1, $head_format);
- $worksheet1->write_blank(1, 2, $head_format);
- $worksheet1->write_blank(1, 3, $head_format);
-
- $worksheet1->set_column(0, 0, 70);
- $worksheet1->set_column(0, 1, 25);
- $worksheet1->set_column(0, 2, 25);
- $worksheet1->set_column(0, 3, 50);
-
- $row = 2;
-
- foreach ($group_by_data as $group_field => $sem_ids) {
- switch ($this->sem_browse_data['group_by']) {
- case 0:
- $headline = $this->search_obj->sem_dates[$group_field]['name'];
- break;
- case 1:
- if ($the_tree->tree_data[$group_field]) {
- $headline = $the_tree->getShortPath($group_field);
- } else {
- $headline = _('keine Studienbereiche eingetragen');
- }
- break;
- case 3:
- $headline = $SEM_TYPE[$group_field]['name']
- ." ("
- . $SEM_CLASS[$SEM_TYPE[$group_field]['class']]['name'] . ')';
- break;
- default:
- $headline = $group_field;
- }
- ++$row;
- $worksheet1->write_string($row, 0 , mb_convert_encoding($headline, 'WINDOWS-1252'), $caption_format);
- $worksheet1->write_blank($row, 1, $caption_format);
- $worksheet1->write_blank($row, 2, $caption_format);
- $worksheet1->write_blank($row, 3, $caption_format);
- ++$row;
- if (is_array($sem_ids['Seminar_id'])) {
- foreach(array_keys($sem_ids['Seminar_id']) as $seminar_id){
- $seminar_obj = new Seminar($seminar_id);
-
- $sem_name = $seminar_obj->getName();
- $seminar_number = key($sem_data[$seminar_id]['VeranstaltungsNummer']);
- $sem_number_start = key($sem_data[$seminar_id]['sem_number']);
- $sem_number_end = key($sem_data[$seminar_id]['sem_number_end']);
- if ($sem_number_start != $sem_number_end) {
- $sem_name .= ' (' . $this->search_obj->sem_dates[$sem_number_start]['name'] . ' - ';
- $sem_name .= ($sem_number_end == -1 ? _('unbegrenzt') : $this->search_obj->sem_dates[$sem_number_end]['name']) . ')';
- } elseif ($this->sem_browse_data['group_by']) {
- $sem_name .= ' (' . $this->search_obj->sem_dates[$sem_number_start]['name'] . ')';
- }
- // is this sem a studygroup?
- $studygroup_mode = SeminarCategories::GetByTypeId($seminar_obj->getStatus())->studygroup_mode;
- if ($studygroup_mode) {
- $sem_name = $seminar_obj->getName() . ' (' . _('Studiengruppe');
- if ($seminar_obj->admission_prelim) $sem_name .= ', '. _('Zutritt auf Anfrage');
- $sem_name .= ')';
- }
- $worksheet1->write_string($row, 0, mb_convert_encoding($sem_name, 'WINDOWS-1252'), $data_format);
- //create Turnus field
- $temp_turnus_string = $seminar_obj->getFormattedTurnus(true);
- //Shorten, if string too long (add link for details.php)
- if (mb_strlen($temp_turnus_string) > 245) {
- $temp_turnus_string = mb_substr($temp_turnus_string,
- 0, mb_strpos(
- mb_substr($temp_turnus_string, 245,
- mb_strlen($temp_turnus_string)
- ), ','
- ) + 246);
- $temp_turnus_string .= ' ... (' . _('mehr') . ')';
- }
- $worksheet1->write_string($row, 1, mb_convert_encoding($seminar_number, 'WINDOWS-1252'), $data_format);
- $worksheet1->write_string($row, 2, mb_convert_encoding($temp_turnus_string, 'WINDOWS-1252'), $data_format);
-
- $doz_name = [];
- $c = 0;
- reset($sem_data[$seminar_id]['fullname']);
- foreach ($sem_data[$seminar_id]['username'] as $anzahl1) {
- if ($c == 0) {
- $d_name = key($sem_data[$seminar_id]['fullname']);
- $anzahl2 = current($sem_data[$seminar_id]['fullname']);
- next($sem_data[$seminar_id]['fullname']);
- $c = $anzahl2 / $anzahl1;
- $doz_name = array_merge($doz_name, array_fill(0, $c, $d_name));
- }
- --$c;
- }
- $doz_position = array_keys($sem_data[$seminar_id]['position']);
- if (is_array($doz_name)){
- if (count($doz_position) != count($doz_name)) {
- $doz_position = range(1, count($doz_name));
- }
- array_multisort($doz_position, $doz_name);
- $worksheet1->write_string($row, 3, mb_convert_encoding(join(', ', $doz_name), 'WINDOWS-1252'), $data_format);
- }
- ++$row;
- }
- }
- }
- $workbook->close();
- }
- return $tmpfile;
- }
-
public function get_result()
{
global $_fullname_sql, $user;
diff --git a/lib/classes/SemClass.class.php b/lib/classes/SemClass.php
index 09580e2..2a038e2 100644
--- a/lib/classes/SemClass.class.php
+++ b/lib/classes/SemClass.php
@@ -304,9 +304,11 @@ class SemClass implements ArrayAccess
public function isModuleAllowed($modulename)
{
return !$this->isModuleForbidden($modulename)
- && (empty($this->data['modules'][$modulename])
- || !$this->data['modules'][$modulename]['sticky']
- || $this->data['modules'][$modulename]['activated']);
+ && (
+ empty($this->data['modules'][$modulename])
+ || empty($this->data['modules'][$modulename]['sticky'])
+ || !empty($this->data['modules'][$modulename]['activated'])
+ );
}
/**
@@ -317,8 +319,8 @@ class SemClass implements ArrayAccess
public function isModuleMandatory($module)
{
return isset($this->data['modules'][$module])
- && $this->data['modules'][$module]['sticky']
- && $this->data['modules'][$module]['activated'];
+ && !empty($this->data['modules'][$module]['sticky'])
+ && !empty($this->data['modules'][$module]['activated']);
}
public function getSemTypes()
@@ -387,7 +389,7 @@ class SemClass implements ArrayAccess
"chdate = UNIX_TIMESTAMP() " .
"WHERE id = :id ".
"");
- StudipCacheFactory::getCache()->expire('DB_SEM_CLASSES_ARRAY');
+ \Studip\Cache\Factory::getCache()->expire('DB_SEM_CLASSES_ARRAY');
return $statement->execute([
'id' => $this->data['id'],
'name' => $this->data['name'],
@@ -451,7 +453,7 @@ class SemClass implements ArrayAccess
DELETE FROM sem_classes
WHERE id = :id
");
- StudipCacheFactory::getCache()->expire('DB_SEM_CLASSES_ARRAY');
+ \Studip\Cache\Factory::getCache()->expire('DB_SEM_CLASSES_ARRAY');
return $statement->execute([
'id' => $this->data['id']
]);
@@ -478,11 +480,8 @@ class SemClass implements ArrayAccess
* deprecated, does nothing, should not be used
* @param string $offset
* @param mixed $value
- *
- * @todo Add void return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetSet($offset, $value)
+ public function offsetSet($offset, $value): void
{
}
@@ -490,12 +489,8 @@ class SemClass implements ArrayAccess
* Compatibility function with old $SEM_CLASS variable for plugins. Maps the
* new array-structure to the old boolean values.
* @param integer $offset: name of attribute
- * @return boolean|(localized)string
- *
- * @todo Add mixed return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetGet($offset)
+ public function offsetGet($offset): mixed
{
switch ($offset) {
case "name":
@@ -528,12 +523,8 @@ class SemClass implements ArrayAccess
/**
* ArrayAccess method to check if an attribute exists.
* @param int $offset
- * @return bool
- *
- * @todo Add bool return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetExists($offset)
+ public function offsetExists($offset): bool
{
return isset($this->data[$offset]);
}
@@ -541,11 +532,8 @@ class SemClass implements ArrayAccess
/**
* deprecated, does nothing, should not be used
* @param string $offset
- *
- * @todo Add void return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetUnset($offset)
+ public function offsetUnset($offset): void
{
}
@@ -564,7 +552,7 @@ class SemClass implements ArrayAccess
$db = DBManager::get();
self::$sem_classes = [];
- $cache = StudipCacheFactory::getCache();
+ $cache = \Studip\Cache\Factory::getCache();
$class_array = unserialize($cache->read('DB_SEM_CLASSES_ARRAY'));
if (!$class_array) {
@@ -576,7 +564,7 @@ class SemClass implements ArrayAccess
$class_array = $statement->fetchAll(PDO::FETCH_ASSOC);
if ($class_array) {
- $cache = StudipCacheFactory::getCache();
+ $cache = \Studip\Cache\Factory::getCache();
$cache->write('DB_SEM_CLASSES_ARRAY', serialize($class_array));
}
} catch (PDOException $e) {
@@ -605,7 +593,7 @@ class SemClass implements ArrayAccess
*/
static public function refreshClasses()
{
- StudipCacheFactory::getCache()->expire('DB_SEM_CLASSES_ARRAY');
+ \Studip\Cache\Factory::getCache()->expire('DB_SEM_CLASSES_ARRAY');
self::$sem_classes = null;
return self::getClasses();
}
diff --git a/lib/classes/SemType.class.php b/lib/classes/SemType.php
index 3d5de5d..5be1f19 100644
--- a/lib/classes/SemType.class.php
+++ b/lib/classes/SemType.php
@@ -68,7 +68,7 @@ class SemType implements ArrayAccess
"chdate = UNIX_TIMESTAMP() " .
"WHERE id = :id ".
"");
- StudipCacheFactory::getCache()->expire('DB_SEM_TYPES_ARRAY');
+ \Studip\Cache\Factory::getCache()->expire('DB_SEM_TYPES_ARRAY');
return $statement->execute([
'id' => $this->data['id'],
'name' => $this->data['name'],
@@ -86,10 +86,10 @@ class SemType implements ArrayAccess
if ($this->countSeminars() === 0) {
$db = DBManager::get();
$statement = $db->prepare("
- DELETE FROM sem_types
- WHERE id = :id
+ DELETE FROM sem_types
+ WHERE id = :id
");
- StudipCacheFactory::getCache()->expire('DB_SEM_TYPES_ARRAY');
+ \Studip\Cache\Factory::getCache()->expire('DB_SEM_TYPES_ARRAY');
return $statement->execute([
'id' => $this->data['id']
]);
@@ -119,11 +119,9 @@ class SemType implements ArrayAccess
* deprecated, does nothing, should not be used
* @param string $offset
* @param mixed $value
- *
- * @todo Add void return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetSet($offset, $value)
+
+ public function offsetSet($offset, $value): void
{
}
@@ -131,12 +129,8 @@ class SemType implements ArrayAccess
* Compatibility function with old $SEM_TYPE variable for plugins. Maps the
* new array-structure to the old boolean values.
* @param integer $offset: name of attribute
- * @return boolean|(localized)string
- *
- * @todo Add mixed return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetGet($offset)
+ public function offsetGet($offset): mixed
{
switch ($offset) {
case "name":
@@ -153,12 +147,8 @@ class SemType implements ArrayAccess
/**
* ArrayAccess method to check if an attribute exists.
* @param mixed $offset
- * @return bool
- *
- * @todo Add bool return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetExists($offset)
+ public function offsetExists($offset): bool
{
return isset($this->data[$offset]);
}
@@ -166,11 +156,8 @@ class SemType implements ArrayAccess
/**
* deprecated, does nothing, should not be used
* @param string $offset
- *
- * @todo Add void return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetUnset($offset)
+ public function offsetUnset($offset): void
{
}
@@ -188,7 +175,7 @@ class SemType implements ArrayAccess
$db = DBManager::get();
self::$sem_types = [];
- $cache = StudipCacheFactory::getCache();
+ $cache = \Studip\Cache\Factory::getCache();
$types_array = unserialize($cache->read('DB_SEM_TYPES_ARRAY'));
if (!$types_array) {
try {
@@ -198,7 +185,7 @@ class SemType implements ArrayAccess
$statement->execute();
$types_array = $statement->fetchAll(PDO::FETCH_ASSOC);
if ($types_array) {
- $cache = StudipCacheFactory::getCache();
+ $cache = \Studip\Cache\Factory::getCache();
$cache->write('DB_SEM_TYPES_ARRAY', serialize($types_array));
}
} catch (PDOException $e) {
@@ -223,7 +210,7 @@ class SemType implements ArrayAccess
static public function refreshTypes() {
self::$sem_types = null;
- StudipCacheFactory::getCache()->expire('DB_SEM_TYPES_ARRAY');
+ \Studip\Cache\Factory::getCache()->expire('DB_SEM_TYPES_ARRAY');
return self::getTypes();
}
diff --git a/lib/classes/Seminar.class.php b/lib/classes/Seminar.php
index 054c337..0fccdbc 100644
--- a/lib/classes/Seminar.class.php
+++ b/lib/classes/Seminar.php
@@ -4,7 +4,7 @@
# Lifter007: TODO
# Lifter010: TODO
/**
- * Seminar.class.php - This class represents a Seminar in Stud.IP
+ * Seminar.php - This class represents a Seminar in Stud.IP
*
* This class provides functions for seminar-members, seminar-dates, and seminar-modules
*
@@ -303,7 +303,7 @@ class Seminar
{
// Caching
- $cache = StudipCacheFactory::getCache();
+ $cache = \Studip\Cache\Factory::getCache();
$cache_key = 'course/undecorated_data/'. $this->id;
if ($filter) {
@@ -745,7 +745,7 @@ class Seminar
StudipLog::log("SEM_ADD_SINGLEDATE", $this->getId(), $singledate->toString(), 'SingleDateID: '.$singledate->getTerminID());
// logging <<<<<<
- $cache = StudipCacheFactory::getCache();
+ $cache = \Studip\Cache\Factory::getCache();
$cache->expire('course/undecorated_data/'. $this->getId());
$this->readSingleDates();
@@ -1565,7 +1565,7 @@ class Seminar
SeminarCycleDate::deleteBySQL('seminar_id = ' . DBManager::get()->quote($s_id));
// Alle weiteren Postings zu diesem Seminar in den Forums-Modulen löschen
- foreach (PluginEngine::getPlugins('ForumModule') as $plugin) {
+ foreach (PluginEngine::getPlugins(ForumModule::class) as $plugin) {
$plugin->deleteContents($s_id); // delete content irrespective of plugin-activation in the seminar
if ($plugin->isActivated($s_id)) { // only show a message, if the plugin is activated, to not confuse the user
@@ -1735,7 +1735,7 @@ class Seminar
*/
public function getDatesTemplate($template, $params = [])
{
- if (!$template instanceof Flexi_Template && is_string($template)) {
+ if (!$template instanceof Flexi\Template && is_string($template)) {
$template = $GLOBALS['template_factory']->open($template);
}
diff --git a/lib/classes/SeminarCategories.class.php b/lib/classes/SeminarCategories.php
index 5302f1e..25eb4c1 100644
--- a/lib/classes/SeminarCategories.class.php
+++ b/lib/classes/SeminarCategories.php
@@ -3,7 +3,7 @@
# Lifter003: TODO
# Lifter010: TODO
/**
- * SeminarCategories.class.php
+ * SeminarCategories.php
*
* encapsulates configuration settings for courses from config.inc.php
* aka $SEM_CLASS, $SEM_TYPE, $UPLOAD_TYPES
@@ -14,7 +14,7 @@
*/
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// SeminarCategories.class.php
+// SeminarCategories.php
//
// Copyright (C) 2008 André Noack <noack@data-quest>, data-quest GmbH <info@data-quest.de>
// +---------------------------------------------------------------------------+
diff --git a/lib/classes/SessionDecoder.class.php b/lib/classes/SessionDecoder.php
index 8c0bc6b..fc40c07 100644
--- a/lib/classes/SessionDecoder.class.php
+++ b/lib/classes/SessionDecoder.php
@@ -3,7 +3,7 @@
# Lifter003: TODO
# Lifter010: TODO
/**
- * SessionDecoder.class.php
+ * SessionDecoder.php
*
* decodes serialized PHP session data
*
@@ -13,7 +13,7 @@
*/
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// SessionDecoder.class.php
+// SessionDecoder.php
//
// Copyright (C) 2008 André Noack <noack@data-quest>, data-quest GmbH <info@data-quest.de>
// +---------------------------------------------------------------------------+
@@ -83,69 +83,42 @@ class SessionDecoder implements ArrayAccess, Countable, Iterator {
return $this->var_names;
}
- /**
- * @todo Add void return type when Stud.IP requires PHP8 minimal
- */
- #[ReturnTypeWillChange]
- public function rewind()
+ public function rewind(): void
{
reset($this->var_names);
}
- /**
- * @todo Add mixed return type when Stud.IP requires PHP8 minimal
- */
- #[ReturnTypeWillChange]
- public function current()
+ public function current(): mixed
{
$current = current($this->var_names);
return $current !== false ? $this->offsetGet($current) : false;
}
- /**
- * @todo Add mixed return type when Stud.IP requires PHP8 minimal
- */
- #[ReturnTypeWillChange]
- public function key()
+ public function key(): mixed
{
return current($this->var_names);
}
- /**
- * @todo Add void return type when Stud.IP requires PHP8 minimal
- */
- #[ReturnTypeWillChange]
- public function next()
+ public function next(): void
{
next($this->var_names);
}
- /**
- * @todo Add bool return type when Stud.IP requires PHP8 minimal
- */
- #[ReturnTypeWillChange]
- public function valid()
+ public function valid(): bool
{
$current = current($this->var_names);
return $current !== false;
}
- /**
- * @todo Add bool return type when Stud.IP requires PHP8 minimal
- */
- #[ReturnTypeWillChange]
- public function offsetExists($offset)
+ public function offsetExists($offset): bool
{
return isset($this->encoded_session[$offset]);
}
/**
- * @param $offset
- * @return mixed|null
- * @todo Add mixed return type when Stud.IP requires PHP8 minimal
+ * @param string $offset
*/
- #[ReturnTypeWillChange]
- public function offsetGet($offset)
+ public function offsetGet($offset): mixed
{
if($this->offsetExists($offset) && !isset($this->decoded_session[$offset])) {
$this->decoded_session[$offset] = unserialize($this->encoded_session[$offset]);
@@ -153,27 +126,15 @@ class SessionDecoder implements ArrayAccess, Countable, Iterator {
return $this->decoded_session[$offset] ?? null;
}
- /**
- * @todo Add void return type when Stud.IP requires PHP8 minimal
- */
- #[ReturnTypeWillChange]
- public function offsetSet($offset, $value)
+ public function offsetSet($offset, $value): void
{
}
- /**
- * @todo Add void return type when Stud.IP requires PHP8 minimal
- */
- #[ReturnTypeWillChange]
- public function offsetUnset($offset)
+ public function offsetUnset($offset): void
{
}
- /**
- * @todo Add int return type when Stud.IP requires PHP8 minimal
- */
- #[ReturnTypeWillChange]
- public function count()
+ public function count(): int
{
return count($this->var_names);
}
diff --git a/lib/classes/SimpleCollection.php b/lib/classes/SimpleCollection.php
new file mode 100644
index 0000000..ae773f5
--- /dev/null
+++ b/lib/classes/SimpleCollection.php
@@ -0,0 +1,782 @@
+<?php
+if (!defined('SORT_NATURAL')) {
+ define('SORT_NATURAL', 6);
+}
+if (!defined('SORT_FLAG_CASE')) {
+ define('SORT_FLAG_CASE', 8);
+}
+
+/**
+ * SimpleCollection.php
+ * collection of assoc arrays with convenience
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @author André Noack <noack@data-quest.de>
+ * @copyright 2013 Stud.IP Core-Group
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ *
+ * @template T
+ */
+class SimpleCollection extends StudipArrayObject
+{
+ /**
+ * callable to initialize collection
+ *
+ * @var ?callable(): array<T>
+ */
+ protected $finder;
+
+ /**
+ * number of records after last init
+ *
+ * @var int
+ */
+ protected $last_count;
+
+ /**
+ * collection with deleted records
+ * @var static
+ */
+ protected $deleted;
+
+ /**
+ * creates a collection from an array of arrays
+ * all arrays should contain same keys, but is not enforced
+ *
+ * @param array<T> $data array containing assoc arrays
+ * @return SimpleCollection<T>
+ */
+ public static function createFromArray(array $data)
+ {
+ return new self($data);
+ }
+
+ /**
+ * converts arrays or objects to ArrayObject objects
+ * if ArrayAccess interface is not available
+ *
+ * @param mixed $a
+ * @return StudipArrayObject|ArrayAccess
+ */
+ public static function arrayToArrayObject($a)
+ {
+ if ($a instanceof StudipArrayObject) {
+ $a->setFlags(StudipArrayObject::ARRAY_AS_PROPS);
+ return $a;
+ }
+
+ if ($a instanceof ArrayObject) {
+ return new StudipArrayObject($a->getArrayCopy(), StudipArrayObject::ARRAY_AS_PROPS);
+ }
+
+ if ($a instanceof ArrayAccess) {
+ return $a;
+ }
+
+ return new StudipArrayObject((array) $a, StudipArrayObject::ARRAY_AS_PROPS);
+ }
+
+ /**
+ * returns closure to compare a value against given arguments
+ * using given operator
+ *
+ * @param string|callable(mixed, mixed|array): bool $operator
+ * @param mixed|array $args
+ * @throws InvalidArgumentException
+ * @return callable(mixed): bool comparison function
+ */
+ public static function getCompFunc($operator, $args)
+ {
+ if (is_callable($operator)) {
+ $comp_func = function ($a) use ($args, $operator) {
+ return $operator($a, $args);
+ };
+ } else {
+ if (!is_array($args)) {
+ $args = [$args];
+ }
+ switch ($operator) {
+ case '==':
+ $comp_func = function ($a) use ($args) {
+ return in_array($a, $args);
+ };
+ break;
+ case '===':
+ $comp_func = function ($a) use ($args) {
+ return in_array($a, $args, true);
+ };
+ break;
+ case '!=':
+ case '<>':
+ $comp_func = function ($a) use ($args) {
+ return !in_array($a, $args);
+ };
+ break;
+ case '!==':
+ $comp_func = function ($a) use ($args) {
+ return !in_array($a, $args, true);
+ };
+ break;
+ case '<':
+ case '>':
+ case '<=':
+ case '>=':
+ $op_func = function ($a, $b) use ($operator) {
+ if ($operator === '<') {
+ return $a < $b;
+ } elseif ($operator === '<=') {
+ return $a <= $b;
+ } elseif ($operator === '>=') {
+ return $a >= $b;
+ } elseif ($operator === '>') {
+ return $a > $b;
+ }
+ };
+ $comp_func = function ($a) use ($op_func, $args) {
+ return $op_func($a, $args[0]);
+ };
+ break;
+ case '><':
+ $comp_func = function ($a) use ($args) {
+ return $a > $args[0] && $a < $args[1];
+ };
+ break;
+ case '>=<=':
+ $comp_func = function ($a) use ($args) {
+ return $a >= $args[0] && $a <= $args[1];
+ };
+ break;
+ case '%=':
+ $comp_func = function ($a) use ($args) {
+ $a = mb_strtolower(static::translitLatin1($a));
+ $args = array_map([static::class, 'translitLatin1'], $args);
+ $args = array_map('mb_strtolower', $args);
+ return in_array($a, $args);
+ };
+ break;
+ case '*=':
+ $comp_func = function ($a) use ($args) {
+ foreach ($args as $arg) {
+ if (mb_strpos($a, $arg) !== false) {
+ return true;
+ }
+ }
+ return false;
+ };
+ break;
+ case '^=':
+ $comp_func = function ($a) use ($args) {
+ foreach ($args as $arg) {
+ if (mb_strpos($a, $arg) === 0) {
+ return true;
+ }
+ }
+ return false;
+ };
+ break;
+ case '$=':
+ $comp_func = function ($a) use ($args) {
+ foreach ($args as $arg) {
+ $found = mb_strrpos($a, $arg);
+ if ($found !== false && ($found + mb_strlen($arg)) === mb_strlen($a)) {
+ return true;
+ }
+ }
+ return false;
+ };
+ break;
+ case '~=':
+ $comp_func = function ($a) use ($args) {
+ foreach ($args as $arg) {
+ if (preg_match($arg, $a) === 1) {
+ return true;
+ }
+ }
+ return false;
+ };
+ break;
+ default:
+ throw new InvalidArgumentException('unknown operator: ' . $operator);
+ }
+ }
+ return $comp_func;
+ }
+
+ /**
+ * transliterates latin1 string to ascii
+ *
+ * @param string $text
+ * @return string
+ */
+ public static function translitLatin1($text)
+ {
+ if (!preg_match('/[\200-\377]/', $text)) {
+ return $text;
+ }
+ $text = str_replace(['ä','Ä','ö','Ö','ü','Ü','ß'], ['a','A','o','O','u','U','s'], $text);
+ $text = str_replace(['À','Á','Â','Ã','Å','Æ'], 'A' , $text);
+ $text = str_replace(['à','á','â','ã','å','æ'], 'a' , $text);
+ $text = str_replace(['È','É','Ê','Ë'], 'E' , $text);
+ $text = str_replace(['è','é','ê','ë'], 'e' , $text);
+ $text = str_replace(['Ì','Í','Î','Ï'], 'I' , $text);
+ $text = str_replace(['ì','í','î','ï'], 'i' , $text);
+ $text = str_replace(['Ò','Ó','Õ','Ô','Ø'], 'O' , $text);
+ $text = str_replace(['ò','ó','ô','õ','ø'], 'o' , $text);
+ $text = str_replace(['Ù','Ú','Û'], 'U' , $text);
+ $text = str_replace(['ù','ú','û'], 'u' , $text);
+ $text = str_replace(['Ç','ç','Ð','Ñ','Ý','ñ','ý','ÿ'], ['C','c','D','N','Y','n','y','y'] , $text);
+ return $text;
+ }
+
+ /**
+ * Constructor
+ *
+ * @param array<T>|callable(): array<T> $data array or closure to fill collection
+ */
+ public function __construct($data = [])
+ {
+ parent::__construct();
+ $this->finder = is_callable($data) ? $data : null;
+ $this->deleted = clone $this;
+ if (is_callable($data)) {
+ $this->refresh();
+ } else {
+ $this->exchangeArray($data);
+ }
+ }
+
+ /**
+ * @param array $input
+ * @return array
+ */
+ public function exchangeArray($input)
+ {
+ return parent::exchangeArray(array_map(
+ [static::class, 'arrayToArrayObject'],
+ $input
+ ));
+ }
+
+ /**
+ * converts the object and all elements to plain arrays
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ $args = func_get_args();
+ return $this->map(function ($a) use ($args) {
+ if (method_exists($a, 'toArray')) {
+ return call_user_func_array([$a, 'toArray'], $args);
+ }
+ if (method_exists($a, 'getArrayCopy')) {
+ return $a->getArrayCopy();
+ }
+ return (array) $a;
+ }
+ );
+ }
+
+ /**
+ *
+ * @see ArrayObject::append()
+ */
+ public function append($newval)
+ {
+ parent::append(static::arrayToArrayObject($newval));
+ }
+
+ /**
+ * Sets the value at the specified index
+ * ensures the value has ArrayAccess
+ *
+ * @param mixed $index
+ * @param mixed $newval
+ *
+ * @see ArrayObject::offsetSet()
+ */
+
+ public function offsetSet($index, $newval): void
+ {
+ if (is_numeric($index)) {
+ $index = (int) $index;
+ }
+ parent::offsetSet($index, static::arrayToArrayObject($newval));
+ }
+
+ /**
+ * Unsets the value at the specified index
+ * value is moved to internal deleted collection
+ *
+ * @see ArrayObject::offsetUnset()
+ * @throws InvalidArgumentException
+ */
+ public function offsetUnset($index): void
+ {
+ if ($this->offsetExists($index)) {
+ $this->deleted[] = $this->offsetGet($index);
+ }
+ parent::offsetUnset($index);
+ }
+
+ /**
+ * sets the finder function
+ *
+ * @param callable(): array<T> $finder
+ * @return void
+ */
+ public function setFinder(callable $finder)
+ {
+ $this->finder = $finder;
+ }
+
+ /**
+ * get deleted records collection
+ * @return SimpleCollection<T>
+ */
+ public function getDeleted()
+ {
+ return $this->deleted;
+ }
+
+ /**
+ * reloads the elements of the collection
+ * by calling the finder function
+ *
+ * @return ?int of records after refresh
+ */
+ public function refresh()
+ {
+ if (is_callable($this->finder)) {
+ $data = call_user_func($this->finder);
+ $this->exchangeArray($data);
+ $this->deleted->exchangeArray([]);
+ return $this->last_count = $this->count();
+ }
+ }
+
+ /**
+ * returns a new collection containing all elements
+ * where given columns value matches given value(s) using passed operator
+ * pass array for multiple values
+ *
+ * operators:
+ * == equal, like php
+ * === identical, like php
+ * !=,<> not equal, like php
+ * !== not identical, like php
+ * <,>,<=,>= less,greater,less or equal,greater or equal
+ * >< between without borders, needs two arguments
+ * >=<= between including borders, needs two arguments
+ * %= like string, transliterate to ascii,case insensitive
+ * *= contains string
+ * ^= begins with string
+ * $= ends with string
+ * ~= regex
+ *
+ * @param string $key the column name
+ * @param mixed $values value to search for
+ * @param string|callable $op operator to find
+ * @return SimpleCollection<T> with found records
+ */
+ public function findBy($key, $values, $op = '==')
+ {
+ $comp_func = self::getCompFunc($op, $values);
+ return $this->filter(function ($record) use ($comp_func, $key) {
+ return $comp_func($record[$key]);
+ });
+ }
+
+ /**
+ * returns the first element
+ * where given column has given value(s)
+ * pass array for multiple values
+ *
+ * @param string $key the column name
+ * @param mixed $values value to search for,
+ * @param string|callable $op operator to find
+ * @return ?T found record
+ */
+ public function findOneBy($key, $values, $op = '==')
+ {
+ $comp_func = self::getCompFunc($op, $values);
+ return $this->filter(function ($record) use ($comp_func, $key) {
+ return $comp_func($record[$key]);
+ }, 1)->first();
+ }
+
+ /**
+ * apply given callback to all elements of
+ * collection
+ *
+ * @param callable(T): int $func the function to call
+ * @return int|false addition of return values
+ */
+ public function each(callable $func)
+ {
+ $result = false;
+ foreach ($this->storage as $record) {
+ $result += call_user_func($func, $record);
+ }
+ return $result;
+ }
+
+ /**
+ * apply given callback to all elements of
+ * collection and give back array of return values
+ *
+ * @param callable(T, mixed): mixed $func the function to call
+ * @return array<mixed>
+ */
+ public function map(callable $func)
+ {
+ $results = [];
+ foreach ($this->storage as $key => $value) {
+ $results[$key] = call_user_func($func, $value, $key);
+ }
+ return $results;
+ }
+
+ /**
+ * filter elements
+ * if given callback returns true
+ *
+ * @param ?callable(T, mixed): bool $func the function to call
+ * @param ?integer $limit limit number of found records
+ * @return SimpleCollection<T> containing filtered elements
+ */
+ public function filter(callable $func = null, $limit = null)
+ {
+ $results = [];
+ $found = 0;
+ foreach ($this->storage as $key => $value) {
+ if (call_user_func($func, $value, $key)) {
+ $results[$key] = $value;
+ if ($limit && (++$found == $limit)) {
+ break;
+ }
+ }
+ }
+ return self::createFromArray($results);
+ }
+
+ /**
+ * Returns whether any element of the collection returns true for the
+ * given callback.
+ *
+ * @param callable(T, mixed): bool $func the function to call
+ * @return bool
+ */
+ public function any(callable $func)
+ {
+ foreach ($this->storage as $key => $value) {
+ if (call_user_func($func, $value, $key)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether every element of the collection returns true for the
+ * given callback.
+ *
+ * @param callable(T, mixed): bool $func the function to call
+ * @return bool
+ */
+ public function every(callable $func)
+ {
+ foreach ($this->storage as $key => $value) {
+ if (!call_user_func($func, $value, $key)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * extract array of columns values
+ * pass array or space-delimited string for multiple columns
+ *
+ * @param string|array $columns the column(s) to extract
+ * @return array of extracted values
+ */
+ public function pluck($columns)
+ {
+ if (!is_array($columns)) {
+ $columns = words($columns);
+ }
+ $func = function ($r) use ($columns) {
+ $result = [];
+ foreach ($columns as $c) {
+ $result[] = $r[$c];
+ }
+ return $result;
+ };
+ $result = $this->map($func);
+ return count($columns) === 1 ? array_map('current', $result) : $result;
+ }
+
+ /**
+ * returns the collection as grouped array
+ * first param is the column to group by, it becomes the key in
+ * the resulting array, default is pk. Limit returned fields with second param
+ * The grouped entries can optoionally go through the given
+ * callback. If no callback is provided, only the first grouped
+ * entry is returned, suitable for grouping by unique column
+ *
+ * @param string $group_by the column to group by, pk if ommitted
+ * @param string|array|null $only_these_fields limit returned fields
+ * @param ?callable $group_func closure to aggregate grouped entries
+ * @return array assoc array
+ */
+ public function toGroupedArray($group_by = 'id', $only_these_fields = null, callable $group_func = null)
+ {
+ $result = [];
+ if (is_string($only_these_fields)) {
+ $only_these_fields = words($only_these_fields);
+ }
+ foreach ($this->toArray() as $record) {
+ $key = $record[$group_by];
+ $ret = [];
+ if (is_array($only_these_fields)) {
+ $result[$key][] = array_intersect_key($record, array_flip($only_these_fields));
+ } else {
+ $result[$key][] = $record;
+ }
+ }
+ if ($group_func === null) {
+ $group_func = 'current';
+ }
+ return array_map($group_func, $result);
+ }
+
+ /**
+ * get the first element
+ *
+ * @return ?T first element or null
+ */
+ public function first()
+ {
+ $keys = array_keys($this->storage);
+ $first_offset = reset($keys);
+ return $this->offsetGet($first_offset ?: 0);
+ }
+
+ /**
+ * get the last element
+ *
+ * @return ?T last element or null
+ */
+ public function last()
+ {
+ $keys = array_keys($this->storage);
+ $last_offset = end($keys);
+ return $this->offsetGet($last_offset ?: 0);
+ }
+
+ /**
+ * get the the value from given key from first element
+ *
+ * @param string $key
+ * @return mixed
+ */
+ public function val($key)
+ {
+ $first = $this->first();
+ return $first[$key] ?? null;
+ }
+
+ /**
+ * mark element(s) for deletion
+ * where given column has given value(s)
+ * element(s) are moved to
+ * internal deleted collection
+ * pass array for multiple values
+ *
+ * operators:
+ * == equal, like php
+ * === identical, like php
+ * !=,<> not equal, like php
+ * !== not identical, like php
+ * <,>,<=,>= less,greater,less or equal,greater or equal
+ * >< between without borders, needs two arguments
+ * >=<= between including borders, needs two arguments
+ * %= like string, transliterate to ascii,case insensitive
+ * *= contains string
+ * ^= begins with string
+ * $= ends with string
+ * ~= regex
+ *
+ * @param string $key
+ * @param mixed $values
+ * @param string|callable(mixed, mixed|array): bool $op operator to find elements
+ * @return int|false number of unsetted elements
+ */
+ public function unsetBy($key, $values, $op = '==')
+ {
+ $ret = false;
+ $comp_func = self::getCompFunc($op, $values);
+ foreach ($this->storage as $k => $record) {
+ if ($comp_func($record[$key])) {
+ $this->offsetunset($k);
+ $ret += 1;
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * sorts the collection by columns of contained elements and returns it
+ *
+ * works like sql order by:
+ * first param is a string containing combinations of column names
+ * and sort direction, separated by comma e.g.
+ * 'name asc, nummer desc '
+ * sorts first by name ascending and then by nummer descending
+ * second param denotes the sort type (using PHP sort constants):
+ * SORT_LOCALE_STRING:
+ * compare items as strings, transliterate latin1 to ascii, case insensitiv, natural order for numbers
+ * SORT_NUMERIC:
+ * compare items as integers
+ * SORT_STRING:
+ * compare items as strings
+ * SORT_NATURAL:
+ * compare items as strings using "natural ordering"
+ * SORT_FLAG_CASE:
+ * can be combined (bitwise OR) with SORT_STRING or SORT_NATURAL to sort strings case-insensitively
+ *
+ * @param string $order columns to order by
+ * @param integer $sort_flags
+ * @return $this the sorted collection
+ */
+ public function orderBy($order, $sort_flags = SORT_LOCALE_STRING)
+ {
+ //('name asc, nummer desc ')
+ $sort_locale = false;
+ switch ($sort_flags) {
+ case SORT_NATURAL:
+ $sort_func = 'strnatcmp';
+ break;
+ case SORT_NATURAL | SORT_FLAG_CASE:
+ $sort_func = 'strnatcasecmp';
+ break;
+ case SORT_STRING | SORT_FLAG_CASE:
+ $sort_func = 'strcasecmp';
+ break;
+ case SORT_STRING:
+ $sort_func = 'strcmp';
+ break;
+ case SORT_NUMERIC:
+ $sort_func = function ($a, $b) {
+ return (int) $a - (int) $b;
+ };
+ break;
+ case SORT_LOCALE_STRING:
+ default:
+ $sort_func = 'strnatcasecmp';
+ $sort_locale = true;
+ }
+
+ $sorter = [];
+ foreach (explode(',', $order) as $one) {
+ $sorter[] = array_values(array_filter(array_map('trim', explode(' ', $one))));
+ }
+
+ $func = function ($d1, $d2) use ($sorter, $sort_func, $sort_locale) {
+ do {
+ $current_sorter = current($sorter);
+ $field = $current_sorter[0];
+ $dir = $current_sorter[1] ?? '';
+ if (!$sort_locale) {
+ $value1 = $d1[$field];
+ $value2 = $d2[$field];
+ } else {
+ $value1 = static::translitLatin1(mb_substr($d1[$field], 0, 100));
+ $value2 = static::translitLatin1(mb_substr($d2[$field], 0, 100));
+ }
+ $ret = $sort_func($value1, $value2);
+ if (strtolower($dir) == 'desc') $ret = $ret * -1;
+ } while ($ret === 0 && next($sorter));
+
+ return $ret;
+ };
+ if (count($sorter)) {
+ $this->uasort($func);
+ }
+ return $this;
+ }
+
+ /**
+ * returns a new collection contaning a sequence of original collection
+ * mimics the sql limit constrain:
+ * used with one parameter, the first x elements are extracted
+ * used with two parameters, the first parameter denotes the offset, the second the
+ * number of elements
+ *
+ * @param integer $arg1
+ * @param ?integer $arg2
+ * @return SimpleCollection<T>
+ */
+ public function limit($arg1, $arg2 = null)
+ {
+ if (is_null($arg2)) {
+ if ($arg1 > 0) {
+ $row_count = $arg1;
+ $offset = 0;
+ } else {
+ $row_count = abs($arg1);
+ $offset = $arg1;
+ }
+ } else {
+ $offset = $arg1;
+ $row_count = $arg2;
+ }
+ return self::createFromArray(array_slice($this->storage, $offset, $row_count, true));
+ }
+
+ /**
+ * calls the given method on all elements
+ * of the collection
+ * @param literal-string $method methodname to call
+ * @param array $params parameters for methodcall
+ * @return array of all return values
+ */
+ public function sendMessage($method, $params = []) {
+ $results = [];
+ foreach ($this->storage as $record) {
+ $results[] = call_user_func_array([$record, $method], $params);
+ }
+ return $results;
+ }
+
+ /**
+ * magic version of sendMessage
+ * calls undefineds methods on all elements of the collection
+ * But beware of the dark side...
+ *
+ * @param literal-string $method methodname to call
+ * @param array $params parameters for methodcall
+ * @return array of all return values
+ */
+ public function __call($method, $params)
+ {
+ return $this->sendMessage($method, $params);
+ }
+
+ /**
+ * merge in another collection, elements are appended
+ *
+ * @param SimpleCollection<T> $a_collection
+ * @return void
+ */
+ public function merge(SimpleCollection $a_collection)
+ {
+ $this->storage = array_merge($this->storage, $a_collection->getArrayCopy());
+ }
+}
diff --git a/lib/classes/SimpleORMap.php b/lib/classes/SimpleORMap.php
new file mode 100644
index 0000000..7492906
--- /dev/null
+++ b/lib/classes/SimpleORMap.php
@@ -0,0 +1,2483 @@
+<?php
+/**
+ * SimpleORMap.php
+ * simple object-relational mapping
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @author André Noack <noack@data-quest.de>
+ * @copyright 2010 Stud.IP Core-Group
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+*/
+
+class SimpleORMap implements ArrayAccess, Countable, IteratorAggregate
+{
+ /**
+ * Defines `_` as character used when joining composite primary keys.
+ */
+ const ID_SEPARATOR = '_';
+
+ /**
+ * table row data
+ * @var array $content
+ */
+ protected $content = [];
+
+ /**
+ * table row data
+ * @var array $content_db
+ */
+ protected $content_db = [];
+
+ /**
+ * new state of entry
+ * @var boolean $is_new
+ */
+ protected $is_new = true;
+
+ /**
+ * deleted state of entry
+ * @var boolean $is_deleted
+ */
+ protected $is_deleted = false;
+
+ /**
+ * db table metadata
+ * @var ?array $schemes;
+ */
+ public static $schemes = null;
+
+ /**
+ * configuration data for subclasses
+ * @see self::configure()
+ * @var array $config;
+ */
+ protected static $config = [];
+
+ /**
+ * stores instantiated related objects
+ * @var array $relations
+ */
+ protected $relations = [];
+
+ /**
+ * assoc array for storing values for additional fields
+ *
+ * @var array $additional_data
+ */
+ protected $additional_data = [];
+
+ /**
+ * reserved indentifiers, fields with those names must not have an explicit getXXX() method
+ * @var array $reserved_slots
+ */
+ protected static $reserved_slots = ['value','newid','iterator','tablemetadata', 'relationvalue','wherequery','relationoptions','data','new','id'];
+
+ /**
+ * indicator for batch operations in findEachBySQL
+ *
+ * @var bool $performs_batch_operation
+ */
+ protected static $performs_batch_operation = false;
+
+ /**
+ * name of db table
+ * @return string
+ */
+ protected static function db_table()
+ {
+ return static::config('db_table');
+ }
+
+ /**
+ * table columns
+ * @return array
+ */
+ protected static function db_fields()
+ {
+ return static::config('db_fields');
+ }
+
+ /**
+ * primary key columns
+ * @return array
+ */
+ protected static function pk()
+ {
+ return static::config('pk');
+ }
+
+ /**
+ * default values for columns
+ * @return array
+ */
+ protected static function default_values()
+ {
+ return static::config('default_values');
+ }
+
+ /**
+ * list of columns to deserialize
+ * @return array key is name of column, value is name of ArrayObject class
+ */
+ protected static function serialized_fields()
+ {
+ return static::config('serialized_fields');
+ }
+
+ /**
+ * aliases for columns
+ * alias => column
+ * @return array
+ */
+ protected static function alias_fields()
+ {
+ return static::config('alias_fields');
+ }
+
+ /**
+ * multi-language fields
+ * name => boolean
+ * @return array
+ */
+ protected static function i18n_fields()
+ {
+ return static::config('i18n_fields');
+ }
+
+ /**
+ * additional computed fields
+ * name => callable
+ * @return array
+ */
+ protected static function additional_fields()
+ {
+ return static::config('additional_fields');
+ }
+
+ /**
+ * 1:n relation
+ * @return array
+ */
+ protected static function has_many()
+ {
+ return static::config('has_many');
+ }
+
+ /**
+ * 1:1 relation
+ * @return array
+ */
+ protected static function has_one()
+ {
+ return static::config('has_one');
+ }
+
+ /**
+ * n:1 relations
+ * @return array
+ */
+ protected static function belongs_to()
+ {
+ return static::config('belongs_to');
+ }
+
+ /**
+ * n:m relations
+ * @return array
+ */
+ protected static function has_and_belongs_to_many()
+ {
+ return static::config('has_and_belongs_to_many');
+ }
+
+ /**
+ * callbacks
+ * @return array<string, array<string|Closure>>
+ */
+ protected static function registered_callbacks()
+ {
+ return static::config('registered_callbacks');
+ }
+
+ /**
+ * contains an array of all used identifiers for fields
+ * (db columns + aliased columns + additional columns + relations)
+ * @return array
+ */
+ protected static function known_slots()
+ {
+ return static::config('known_slots');
+ }
+
+ /**
+ * assoc array used to map SORM callback to NotificationCenter
+ * keys are SORM callbacks, values notifications
+ * eg. 'after_create' => 'FooDidCreate'
+ *
+ * @return array
+ */
+ protected static function notification_map()
+ {
+ return static::config('notification_map');
+ }
+
+ /**
+ * assoc array for mapping get/set Methods
+ *
+ * @return array
+ */
+ protected static function getter_setter_map()
+ {
+ return static::config('getter_setter_map');
+ }
+
+ //////////////////////////////////////////////////
+
+ /**
+ * set configuration data from subclass
+ *
+ * @param ?array $config configuration data
+ * @return void
+ */
+ protected static function configure($config = [])
+ {
+ $class = static::class;
+
+ if (empty($config['db_table'])) {
+ $config['db_table'] = strtolower($class);
+ }
+
+ if (!isset($config['db_fields'])) {
+ if (static::tableScheme($config['db_table'])) {
+ $config['db_fields'] = self::$schemes[$config['db_table']]['db_fields'];
+ $config['pk'] = self::$schemes[$config['db_table']]['pk'];
+ }
+ }
+
+ if (isset($config['pk'])
+ && !isset($config['db_fields']['id'])
+ && !isset($config['alias_fields']['id'])
+ && !isset($config['additional_fields']['id'])
+ ) {
+ if (count($config['pk']) === 1) {
+ $config['alias_fields']['id'] = $config['pk'][0];
+ } else {
+ $config['additional_fields']['id'] = ['get' => '_getId',
+ 'set' => '_setId'];
+ }
+ }
+ if (isset($config['additional_fields'])) {
+ foreach ($config['additional_fields'] as $a_field => $a_config) {
+ if (is_array($a_config) && !(isset($a_config['get']) || isset($a_config['set']))) {
+ $relation = $a_config[0] ?? '';
+ $relation_field = $a_config[1] ?? '';
+ if (!$relation) {
+ [$relation, $relation_field] = explode('_', $a_field);
+ }
+ if (!$relation_field || !$relation) {
+ throw new UnexpectedValueException('no relation found for autoget/set additional field: ' . $a_field);
+ }
+ $config['additional_fields'][$a_field] = ['get' => '_getAdditionalValueFromRelation',
+ 'set' => '_setAdditionalValue',
+ 'relation' => $relation,
+ 'relation_field' => $relation_field];
+ }
+ }
+ }
+ if (isset($config['serialized_fields'])) {
+ foreach ($config['serialized_fields'] as $a_field => $object_type) {
+ if (!(is_subclass_of($object_type, 'StudipArrayObject'))) {
+ throw new UnexpectedValueException(sprintf('serialized field %s must use subclass of StudipArrayObject', $a_field));
+ }
+ }
+ }
+
+ foreach (['default_values', 'serialized_fields', 'alias_fields', 'i18n_fields', 'additional_fields'] as $fields) {
+ if (!isset($config[$fields])) {
+ $config[$fields] = [];
+ }
+ }
+
+ foreach (['has_many', 'belongs_to', 'has_one', 'has_and_belongs_to_many'] as $type) {
+ if (isset($config[$type]) && is_array($config[$type])) {
+ foreach (array_keys($config[$type]) as $one) {
+ $config['relations'][$one] = null;
+ }
+ } else {
+ $config[$type] = [];
+ }
+ }
+
+ $callbacks = ['before_create',
+ 'before_update',
+ 'before_store',
+ 'before_delete',
+ 'before_initialize',
+ 'after_create',
+ 'after_update',
+ 'after_store',
+ 'after_delete',
+ 'after_initialize'];
+
+ foreach ($callbacks as $callback) {
+ if (!isset($config['registered_callbacks'][$callback])) {
+ $config['registered_callbacks'][$callback] = [];
+ }
+ }
+
+ $auto_notification_map['after_create'] = $class . 'DidCreate';
+ $auto_notification_map['after_store'] = $class . 'DidStore';
+ $auto_notification_map['after_delete'] = $class . 'DidDelete';
+ $auto_notification_map['after_update'] = $class . 'DidUpdate';
+ $auto_notification_map['before_create'] = $class . 'WillCreate';
+ $auto_notification_map['before_store'] = $class . 'WillStore';
+ $auto_notification_map['before_delete'] = $class . 'WillDelete';
+ $auto_notification_map['before_update'] = $class . 'WillUpdate';
+
+ foreach ($auto_notification_map as $cb => $notification) {
+ if (isset($config['notification_map'][$cb])) {
+ if (strpos($config['notification_map'][$cb], $notification) !== false) {
+ $config['notification_map'][$cb] .= ' ' . $notification;
+ }
+ } else {
+ $config['notification_map'][$cb] = $notification;
+ }
+ }
+
+ if (is_array($config['notification_map'])) {
+ foreach (array_keys($config['notification_map']) as $cb) {
+ $config['registered_callbacks'][$cb][] = 'cbNotificationMapper';
+ }
+ }
+
+ if (!I18N::isEnabled() || empty($config['i18n_fields'])) {
+ $config['i18n_fields'] = [];
+ } elseif (is_string($config['i18n_fields'])) {
+ $i18n_fields = words($config['i18n_fields']);
+ $config['i18n_fields'] = array_combine(
+ $i18n_fields,
+ array_fill(0, count($i18n_fields), true)
+ );
+ } elseif (array_is_list($config['i18n_fields'])) {
+ $config['i18n_fields'] = array_combine(
+ $config['i18n_fields'],
+ array_fill(0, count($config['i18n_fields']), true)
+ );
+ }
+
+ array_unshift($config['registered_callbacks']['after_initialize'], 'cbAfterInitialize');
+
+ $config['known_slots'] = array_merge(
+ array_keys($config['db_fields']),
+ array_keys($config['alias_fields'] ?? []),
+ array_keys($config['additional_fields'] ?? []),
+ array_keys($config['relations'] ?? [])
+ );
+
+ foreach (array_map('strtolower', get_class_methods($class)) as $method) {
+ if (in_array(substr($method, 0, 3), ['get', 'set'])) {
+ $verb = substr($method, 0, 3);
+ $name = substr($method, 3);
+ if (in_array($name, $config['known_slots']) && !in_array($name, static::$reserved_slots) && !isset($config['additional_fields'][$name][$verb])) {
+ $config['getter_setter_map'][$name][$verb] = $method;
+ }
+ }
+ }
+ self::$config[$class] = $config;
+ }
+
+ /**
+ * fetch config data for the called class
+ *
+ * @param string $key config key
+ * @return mixed value of config key (null if not set)
+ */
+ protected static function config($key)
+ {
+ if (!array_key_exists(static::class, self::$config)) {
+ static::configure();
+ }
+
+ return self::$config[static::class][$key] ?? null;
+ }
+
+ /**
+ * fetch table metadata from db or from local cache
+ *
+ * @param string $db_table
+ * @return bool true if metadata could be fetched
+ */
+ public static function tableScheme($db_table)
+ {
+ if (self::$schemes === null) {
+ $cache = \Studip\Cache\Factory::getCache();
+ self::$schemes = unserialize($cache->read('DB_TABLE_SCHEMES'));
+ }
+ if (!isset(self::$schemes[$db_table])) {
+ $db = DBManager::get()->query("SHOW COLUMNS FROM $db_table");
+ while($rs = $db->fetch(PDO::FETCH_ASSOC)){
+ $db_fields[strtolower($rs['Field'])] = [
+ 'name' => $rs['Field'],
+ 'null' => $rs['Null'],
+ 'default' => $rs['Default'],
+ 'type' => $rs['Type'],
+ 'extra' => $rs['Extra']
+ ];
+ if ($rs['Key'] == 'PRI'){
+ $pk[] = strtolower($rs['Field']);
+ }
+ }
+ self::$schemes[$db_table]['db_fields'] = $db_fields;
+ self::$schemes[$db_table]['pk'] = $pk;
+ $cache = \Studip\Cache\Factory::getCache();
+ $cache->write('DB_TABLE_SCHEMES', serialize(self::$schemes));
+ }
+ return isset(self::$schemes[$db_table]);
+ }
+
+ /**
+ * force reload of cached table metadata
+ * @return void
+ */
+ public static function expireTableScheme()
+ {
+ \Studip\Cache\Factory::getCache()->expire('DB_TABLE_SCHEMES');
+ self::$schemes = null;
+ self::$config = [];
+ }
+
+ /**
+ * Returns new instance for given key when found in the database, else null.
+ *
+ * @param string|array $id primary key
+ * @return static|null
+ */
+ public static function find($id)
+ {
+ $ref = new ReflectionClass(static::class);
+ /** @var static $record */
+ $record = $ref->newInstanceArgs(func_get_args());
+ if (!$record->isNew()) {
+ return $record;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns true if given key exists in the database.
+ *
+ * @param string|array $id primary key
+ * @return boolean
+ */
+ public static function exists($id)
+ {
+ $ret = false;
+ $db_table = static::db_table();
+ $record = new static();
+ $record->setId(...func_get_args());
+ $where_query = $record->getWhereQuery();
+ if ($where_query) {
+ $query = "SELECT 1 FROM `$db_table` WHERE "
+ . join(" AND ", $where_query);
+ $ret = (bool)DBManager::get()->query($query)->fetchColumn();
+ }
+ return $ret;
+ }
+
+ /**
+ * returns number of records
+ *
+ * @param ?string $sql sql clause to use on the right side of WHERE
+ * @param ?array $params params for query
+ * @return int
+ */
+ public static function countBySql($sql = '1', $params = [])
+ {
+ $db_table = static::db_table();
+ $db = DBManager::get();
+ $has_join = stripos($sql, 'JOIN ');
+ if ($has_join === false || $has_join > 10) {
+ $sql = 'WHERE ' . $sql;
+ }
+ $sql = "SELECT count(*) FROM `" . $db_table . "` " . $sql;
+ $st = $db->prepare($sql);
+ $st->execute($params);
+ return (int)$st->fetchColumn();
+ }
+
+ /**
+ * creates new record with given data in db
+ * returns the new object or null
+ * @param array $data assoc array of record
+ * @return ?static
+ */
+ public static function create($data)
+ {
+ $record = new static();
+ $record->setData($data, false);
+ if ($record->store()) {
+ return $record;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * build object with given data
+ *
+ * @param array $data assoc array of record
+ * @param ?bool $is_new set object to new state
+ * @return static
+ */
+ public static function build($data, $is_new = true)
+ {
+ $record = new static();
+ $record->setData($data, !$is_new);
+ $record->setNew($is_new);
+ return $record;
+ }
+
+ /**
+ * build object with given data and mark it as existing
+ *
+ * @param array $data assoc array of record
+ * @return static
+ */
+ public static function buildExisting($data)
+ {
+ return static::build($data, false);
+ }
+
+ /**
+ * generate SimpleORMap object structure from assoc array
+ * if given array contains data of related objects in sub-arrays
+ * they are also generated. Existing records are updated, new records are created
+ * (but changes are not yet stored)
+ *
+ * @param array $data
+ * @return static
+ */
+ public static function import($data)
+ {
+ $record_data = [];
+ $relation_data = [];
+ foreach ($data as $key => $value) {
+ $temp = static::alias_fields()[$key] ?? $key;
+ if (isset(static::db_fields()[$temp])) {
+ $record_data[$key] = $value;
+ } else {
+ $relation_data[$key] = $value;
+ }
+ }
+ $record = static::toObject($record_data);
+ if (!$record instanceof static) {
+ $record = new static();
+ $record->setData($record_data, true);
+ } else {
+ $record->setData($record_data);
+ }
+ foreach ($relation_data as $relation => $data) {
+ if (!$record->isRelation($relation)) {
+ continue;
+ }
+
+ $options = $record->getRelationOptions($relation);
+ if ($options['type'] == 'has_one') {
+ $record->{$relation} = call_user_func([$options['class_name'], 'import'], $data);
+ }
+ if ($options['type'] == 'has_many' || $options['type'] == 'has_and_belongs_to_many') {
+ foreach ($data as $one) {
+ $current = call_user_func([$options['class_name'], 'import'], $one);
+ if ($options['type'] == 'has_many') {
+ $foreign_key_value = call_user_func($options['assoc_func_params_func'], $record);
+ call_user_func($options['assoc_foreign_key_setter'], $current, $foreign_key_value);
+ }
+ if ($current->id !== null) {
+ $existing = $record->{$relation}->find($current->id);
+ if ($existing) {
+ $existing->setData($current);
+ } else {
+ $record->{$relation}->append($current);
+ }
+ } else {
+ $record->{$relation}->append($current);
+ }
+ }
+ }
+ }
+ return $record;
+ }
+
+ /**
+ * returns array of instances of given class filtered by given sql
+ * @param string $sql sql clause to use on the right side of WHERE
+ * @param ?array $params parameters for query
+ * @return static[] array of "self" objects
+ */
+ public static function findBySQL($sql, $params = [])
+ {
+ $db_table = static::db_table();
+
+ $has_join = stripos($sql, 'JOIN ');
+ if ($has_join === false || $has_join > 10) {
+ $sql = 'WHERE ' . $sql;
+ }
+ $sql = "SELECT `" . $db_table . "`.* FROM `" . $db_table . "` " . $sql;
+ $stmt = DBManager::get()->prepare($sql);
+ $stmt->execute($params);
+
+ $record = static::build([], false);
+
+ $ret = [];
+ do {
+ $clone = clone $record;
+ $clone->setNew(false);
+ $stmt->setFetchMode(PDO::FETCH_INTO, $clone);
+
+ if ($clone = $stmt->fetch()) {
+ $clone->applyCallbacks('after_initialize');
+ $ret[] = $clone;
+ }
+ } while ($clone);
+ return $ret;
+ }
+
+ /**
+ * returns one instance of given class filtered by given sql
+ * only first row of query is used
+ * @param string $where sql clause to use on the right side of WHERE
+ * @param ?array $params parameters for query
+ * @return ?static
+ */
+ public static function findOneBySQL($where, $params = [])
+ {
+ if (stripos($where, 'LIMIT') === false) {
+ $where .= " LIMIT 1";
+ }
+ $found = static::findBySQL($where, $params);
+ return isset($found[0]) ? $found[0] : null;
+ }
+
+ /**
+ * find related records for a n:m relation (has_many_and_belongs_to_many)
+ * using a combination table holding the keys
+ *
+ * @param string $foreign_key_value value of foreign key to find related records
+ * @param array $options relation options from other side of relation
+ * @return static[] array of "self" objects
+ */
+ public static function findThru($foreign_key_value, $options)
+ {
+ $thru_table = $options['thru_table'];
+ $thru_key = $options['thru_key'];
+ $thru_assoc_key = $options['thru_assoc_key'];
+ $assoc_foreign_key = $options['assoc_foreign_key'];
+
+ $db_table = static::db_table();
+
+ $sql = "SELECT `$db_table`.* FROM `$thru_table`
+ INNER JOIN `$db_table` ON `$thru_table`.`$thru_assoc_key` = `$db_table`.`$assoc_foreign_key`
+ WHERE `$thru_table`.`$thru_key` = ? " . ($options['order_by'] ?? '');
+ $st = DBManager::get()->prepare($sql);
+ $st->execute([$foreign_key_value]);
+
+ $record = static::build([], false);
+
+ $ret = [];
+ do {
+ $clone = clone $record;
+ $clone->setNew(false);
+ $st->setFetchMode(PDO::FETCH_INTO, $clone);
+
+ if ($clone = $st->fetch()) {
+ $clone->applyCallbacks('after_initialize');
+ $ret[] = $clone;
+ }
+ } while ($clone);
+ return $ret;
+ }
+
+ /**
+ * passes objects for given sql through given callback
+ *
+ * @param callable $callable callback which gets the current record as param
+ * @param string $sql where clause of sql
+ * @param ?array $params sql statement parameters
+ * @return integer number of found records
+ */
+ public static function findEachBySQL($callable, $sql, $params = [])
+ {
+ $has_join = stripos($sql, 'JOIN ');
+ if ($has_join === false || $has_join > 10) {
+ $sql = "WHERE {$sql}";
+ }
+
+ $db_table = static::db_table();
+ $st = DBManager::get()->prepare("SELECT `{$db_table}`.* FROM `{$db_table}` {$sql}");
+ $st->execute($params);
+
+ // Indicate that we are performing a batch operation
+ static::$performs_batch_operation = true;
+
+ $record = static::build([], false);
+
+ $ret = 0;
+ do {
+ $clone = clone $record;
+ $clone->setNew(false);
+ $st->setFetchMode(PDO::FETCH_INTO, $clone);
+
+ if ($clone = $st->fetch()) {
+ $clone->applyCallbacks('after_initialize');
+ $callable($clone, $ret++);
+ }
+ } while ($clone);
+
+ // Reset batch operation indicator
+ static::$performs_batch_operation = false;
+
+ return $ret;
+ }
+
+ /**
+ * returns array of instances of given class for by given pks
+ * @param ?array $pks array of primary keys
+ * @param ?string $order order by clause
+ * @param ?array $order_params
+ * @return static[]
+ */
+ public static function findMany($pks = [], $order = '', $order_params = [])
+ {
+ $db_table = static::db_table();
+ $pk = static::pk();
+ $db = DBManager::get();
+ if (count($pk) > 1) {
+ throw new Exception('not implemented yet');
+ }
+ $where = "`$db_table`.`{$pk[0]}` IN (" . $db->quote($pks) . ") ";
+ return static::findBySQL($where . $order, $order_params);
+ }
+
+ /**
+ * passes objects for by given pks through given callback
+ *
+ * @param callable $callable callback which gets the current record as param
+ * @param ?array $pks array of primary keys of called class
+ * @param ?string $order order by sql
+ * @param ?array $order_params
+ * @return integer number of found records
+ */
+ public static function findEachMany($callable, $pks = [], $order = '', $order_params = [])
+ {
+ $db_table = static::db_table();
+ $pk = static::pk();
+ $db = DBManager::get();
+ if (count($pk) > 1) {
+ throw new Exception('not implemented yet');
+ }
+ $where = "`$db_table`.`{$pk[0]}` IN (" . $db->quote($pks) . ") ";
+ return static::findEachBySQL($callable, $where . $order, $order_params);
+ }
+
+ /**
+ * passes objects for given sql through given callback
+ * and returns an array of callback return values
+ *
+ * @param callable $callable callback which gets the current record as param
+ * @param string $where where clause of sql
+ * @param array $params sql statement parameters
+ * @return array return values of callback
+ */
+ public static function findAndMapBySQL($callable, $where, $params = [])
+ {
+ $ret = [];
+ $calleach = function($m) use (&$ret, $callable) {
+ $ret[] = $callable($m);
+ };
+ static::findEachBySQL($calleach, $where, $params);
+ return $ret;
+ }
+
+ /**
+ * passes objects for by given pks through given callback
+ * and returns an array of callback return values
+ *
+ * @param callable $callable callback which gets the current record as param
+ * @param ?array $pks array of primary keys of called class
+ * @param ?string $order order by sql
+ * @param ?array $order_params
+ * @return array return values of callback
+ */
+ public static function findAndMapMany($callable, $pks = [], $order = '', $order_params = [])
+ {
+ $ret = [];
+ $calleach = function($m) use (&$ret, $callable) {
+ $ret[] = $callable($m);
+ };
+ $db_table = static::db_table();
+ $pk = static::pk();
+ $db = DBManager::get();
+ if (count($pk) > 1) {
+ throw new Exception('not implemented yet');
+ }
+ $where = "`$db_table`.`{$pk[0]}` IN (" . $db->quote($pks) . ") ";
+ static::findEachBySQL($calleach, $where . $order, $order_params);
+ return $ret;
+ }
+
+ /**
+ * deletes objects specified by sql clause
+ * @param string $where sql clause to use on the right side of WHERE
+ * @param ?array $params parameters for query
+ * @return integer number of deleted records
+ */
+ public static function deleteBySQL($where, $params = [])
+ {
+ $killeach = function($record) {$record->delete();};
+ return static::findEachBySQL($killeach, $where, $params);
+ }
+
+ /**
+ * returns object of given class for given id or null
+ * the param could be a string, an assoc array containing primary key field
+ * or an already matching object. In all these cases an object is returned
+ *
+ * @param string|static|array $id_or_object id as string, object or assoc array
+ * @return static
+ */
+ public static function toObject($id_or_object)
+ {
+ if ($id_or_object instanceof static) {
+ return $id_or_object;
+ }
+ if (is_array($id_or_object)) {
+ $pk = static::pk();
+ $key_values = [];
+ foreach ($pk as $key) {
+ if (array_key_exists($key, $id_or_object)) {
+ $key_values[] = $id_or_object[$key];
+ }
+ }
+ if (count($pk) === count($key_values)) {
+ if (count($pk) === 1) {
+ $id = $key_values[0];
+ } else {
+ $id = $key_values;
+ }
+ } else {
+ $id = null;
+ }
+ } else {
+ $id = $id_or_object;
+ }
+ return static::find($id);
+ }
+
+ /**
+ * interceptor for static findByColumn / findEachByColumn / countByColumn /
+ * deleteByColumn magic
+ *
+ * @param string $name
+ * @param array $arguments
+ * @throws BadMethodCallException
+ * @return int|static|static[]
+ */
+ public static function __callStatic(string $name, array $arguments)
+ {
+ $db_table = static::db_table();
+ $alias_fields = static::alias_fields();
+ $db_fields = static::db_fields();
+ $name = strtolower($name);
+ $order = '';
+ $param_arr = [];
+ $where = '';
+ $where_param = is_array($arguments[0]) ? $arguments[0] : [$arguments[0]];
+ $action = strstr($name, 'by', true);
+ $field = substr($name, strlen($action) + 2);
+ switch ($action) {
+ case 'findone':
+ $order = $arguments[1] ?? '';
+ $param_arr[0] =& $where;
+ $param_arr[1] = [$where_param];
+ $method = 'findonebysql';
+ break;
+ case 'find':
+ case 'findmany':
+ $order = $arguments[1] ?? '';
+ $param_arr[0] =& $where;
+ $param_arr[1] = [$where_param];
+ $method = 'findbysql';
+ break;
+ case 'findeach':
+ case 'findeachmany':
+ $order = $arguments[2] ?? '';
+ $param_arr[0] = $arguments[0];
+ $param_arr[1] =& $where;
+ $param_arr[2] = [$arguments[1]];
+ $method = 'findeachbysql';
+ break;
+ case 'count':
+ case 'delete':
+ $param_arr[0] =& $where;
+ $param_arr[1] = [$where_param];
+ $method = "{$action}bysql";
+ break;
+ default:
+ throw new BadMethodCallException("Method " . static::class . "::$name not found");
+ }
+ if (isset($alias_fields[$field])) {
+ $field = $alias_fields[$field];
+ }
+ if (isset($db_fields[$field])) {
+ $where = "`$db_table`.`$field` IN(?) " . $order;
+ return call_user_func_array([static::class, $method], $param_arr);
+ }
+ throw new BadMethodCallException("Method " . static::class . "::$name not found");
+ }
+
+ /**
+ * constructor, give primary key of record as param to fetch
+ * corresponding record from db if available, if not preset primary key
+ * with given value. Give null to create new record
+ *
+ * @param null|int|string|array $id primary key of table
+ */
+ function __construct($id = null)
+ {
+ foreach(['has_many', 'belongs_to', 'has_one', 'has_and_belongs_to_many'] as $type) {
+ foreach (array_keys($this->$type()) as $one) {
+ $this->relations[$one] = null;
+ }
+ }
+
+ if ($id) {
+ $this->setId($id);
+ }
+ $this->restore();
+ }
+
+ /**
+ * returns internal used id value (multiple keys concatenated with _)
+ * @param mixed $field unused parameter
+ * @return ?string
+ */
+ protected function _getId($field)
+ {
+ return is_null($this->getId())
+ ? null
+ : implode(self::ID_SEPARATOR, $this->getId());
+ }
+
+ /**
+ * sets internal used id value (multiple keys concatenated with _)
+ * @param string $field Field to set (unused since it's always the id)
+ * @param string $value Value for id field
+ * @return bool
+ */
+ protected function _setId($field, $value)
+ {
+ return $this->setId(explode(self::ID_SEPARATOR, $value));
+ }
+
+ /**
+ * retrieves an additional field value from relation
+ *
+ * @param string $field
+ * @return mixed
+ */
+ protected function _getAdditionalValueFromRelation($field)
+ {
+ [$relation, $relation_field] = [$this->additional_fields()[$field]['relation'],
+ $this->additional_fields()[$field]['relation_field']];
+ if (!array_key_exists($field, $this->additional_data)) {
+ $this->_setAdditionalValue($field, $this->getRelationValue($relation, $relation_field));
+ }
+ return $this->additional_data[$field];
+ }
+
+ /**
+ * sets additional value in field imported from relation
+ *
+ * @param string $field
+ * @param mixed $value
+ * @return mixed
+ */
+ protected function _setAdditionalValueFromRelation($field, $value)
+ {
+ [$relation, $relation_field] = [$this->additional_fields()[$field]['relation'],
+ $this->additional_fields()[$field]['relation_field']];
+ $this->$relation->$field = $value;
+ unset($this->additional_data[$field]);
+ return $this->_getAdditionalValueFromRelation($field);
+ }
+
+ /**
+ * @param string $field
+ * @return mixed
+ */
+ protected function _getAdditionalValue($field)
+ {
+ return $this->additional_data[$field];
+ }
+
+ /**
+ * @param string $field
+ * @param mixed $value
+ * @return mixed
+ */
+ protected function _setAdditionalValue($field, $value)
+ {
+ return $this->additional_data[$field] = $value;
+ }
+
+ /**
+ * clean up references after cloning
+ * @return void
+ */
+ function __clone()
+ {
+ $this->setNew(true);
+ //all references link still to old object => reset all aliases
+ foreach ($this->alias_fields() as $alias => $field) {
+ if (isset($this->db_fields()[$field])) {
+ $content_value = $this->content[$field];
+ $content_db_value = $this->content_db[$field];
+ unset($this->content[$alias]);
+ unset($this->content_db[$alias]);
+ unset($this->content[$field]);
+ unset($this->content_db[$field]);
+ if (is_object($content_value)) {
+ $this->content[$field] = clone $content_value;
+ } else {
+ $this->content[$field] = $content_value;
+ }
+ if (is_object($content_db_value)) {
+ $this->content_db[$field] = clone $content_db_value;
+ } else {
+ $this->content_db[$field] = $content_db_value;
+ }
+ }
+ }
+ foreach ($this->alias_fields() as $alias => $field) {
+ if (isset($this->db_fields()[$field])) {
+ $this->content[$alias] =& $this->content[$field];
+ $this->content_db[$alias] =& $this->content_db[$field];
+ }
+ }
+ //unset all relations for now
+ //TODO: maybe a deep copy of all belonging objects is more appropriate
+ foreach(['has_many', 'belongs_to', 'has_one', 'has_and_belongs_to_many'] as $type) {
+ foreach (array_keys($this->$type()) as $one) {
+ $this->relations[$one] = null;
+ }
+ }
+ //begun the clone war has... hmpf
+ }
+
+ /**
+ * try to determine all needed options for a relationship from
+ * configured options
+ *
+ * @param string $type
+ * @param string $name
+ * @param array $options
+ * @throws Exception if options for thru_table could not be determined
+ * @return array
+ */
+ protected function parseRelationOptions($type, $name, $options)
+ {
+ if (empty($options['class_name'])) {
+ throw new Exception('Option class_name not set for relation ' . $name);
+ }
+ if (empty($options['assoc_foreign_key'])) {
+ if ($type === 'has_many' || $type === 'has_one') {
+ $options['assoc_foreign_key'] = $this->pk()[0];
+ } else if ($type === 'belongs_to') {
+ $options['assoc_foreign_key'] = 'id';
+ }
+ }
+ if ($type === 'has_and_belongs_to_many') {
+ $thru_table = $options['thru_table'];
+ if (empty($options['thru_key'])) {
+ $options['thru_key'] = $this->pk()[0];
+ }
+ if (empty($options['thru_assoc_key']) || empty($options['assoc_foreign_key'])) {
+ $class = $options['class_name'];
+ $record = new $class();
+ $meta = $record->getTableMetadata();
+ if (empty($options['thru_assoc_key'])) {
+ $options['thru_assoc_key'] = $meta['pk'][0];
+ }
+ if (empty($options['assoc_foreign_key'])) {
+ $options['assoc_foreign_key']= $meta['pk'][0];
+ }
+ }
+ static::tableScheme($thru_table);
+ if (is_array(self::$schemes[$thru_table])) {
+ $thru_key_ok = isset(self::$schemes[$thru_table]['db_fields'][$options['thru_key']]);
+ $thru_assoc_key_ok = isset(self::$schemes[$thru_table]['db_fields'][$options['thru_assoc_key']]);
+ }
+ if (!$thru_assoc_key_ok || !$thru_key_ok) {
+ throw new Exception("Could not determine keys for relation " . $name . " through table " . $thru_table);
+ }
+ if ($options['assoc_foreign_key'] instanceof Closure) {
+ throw new Exception("For relation " . $name . " assoc_foreign_key must be a name of a column");
+ }
+ }
+ if (empty($options['assoc_func'])) {
+ if ($type !== 'has_and_belongs_to_many') {
+ $options['assoc_func'] = $options['assoc_foreign_key'] === 'id' ? 'find' : 'findBy' . $options['assoc_foreign_key'];
+ } else {
+ $options['assoc_func'] = 'findThru';
+ }
+ }
+ if (empty($options['foreign_key'])) {
+ $options['foreign_key'] = 'id';
+ }
+ if (isset($options['foreign_key']) && $options['foreign_key'] instanceof Closure) {
+ $options['assoc_func_params_func'] = function($record) use ($name, $options) { return call_user_func($options['foreign_key'], $record, $name, $options);};
+ } else {
+ $options['assoc_func_params_func'] = function($record) use ($name, $options) { return $options['foreign_key'] === 'id' ? $record->getId() : $record->getValue($options['foreign_key']);};
+ }
+ if (isset($options['assoc_foreign_key']) && $options['assoc_foreign_key'] instanceof Closure) {
+ if ($type === 'belongs_to') {
+ $options['assoc_foreign_key_getter'] = function($record, $that) use ($name, $options) { return call_user_func($options['assoc_foreign_key'], $record, $name, $options, $that);};
+ } else {
+ $options['assoc_foreign_key_setter'] = function($record, $params) use ($name, $options) { return call_user_func($options['assoc_foreign_key'], $record, $params, $name, $options);};
+ }
+ } elseif (!empty($options['assoc_foreign_key'])) {
+ if ($type === 'belongs_to') {
+ $options['assoc_foreign_key_getter'] = function($record, $that) use ($name, $options) { return $record->getValue($options['assoc_foreign_key']);};
+ } else {
+ $options['assoc_foreign_key_setter'] = function($record, $value) use ($name, $options) { return $record->setValue($options['assoc_foreign_key'], $value);};
+ }
+ } else {
+ throw new Exception("Could not determine assoc_foreign_key for relation " . $name);
+ }
+ return $options;
+ }
+
+ /**
+ * returns array with option for given relation
+ * available options:
+ * 'type': relation type, on of 'has_many', 'belongs_to', 'has_one', 'has_and_belongs_to_many'
+ * 'class_name': name of class for related records
+ * 'foreign_key': name of column with foreign key
+ * or callback to retrieve foreign key value
+ * 'assoc_foreign_key': name of foreign key column in related class
+ * 'assoc_func': name of static method to call on related class to find related records
+ * 'assoc_func_params_func': callback to retrieve params for assoc_func
+ * 'thru_table': name of relation table for n:m relation
+ * 'thru_key': name of column holding foreign key in relation table
+ * 'thru_assoc_key': name of column holding foreign key from related class in relation table
+ * 'on_delete': contains simply 'delete' to indicate that related records should be deleted
+ * or callback to invoke before record gets deleted
+ * 'on_store': contains simply 'store' to indicate that related records should be stored
+ * or callback to invoke after record gets stored
+ *
+ * @param string $relation name of relation
+ * @return array assoc array containing options
+ */
+ function getRelationOptions($relation)
+ {
+ $options = [];
+ foreach(['has_many', 'belongs_to', 'has_one', 'has_and_belongs_to_many'] as $type) {
+ if (isset($this->$type()[$relation])) {
+ $options = self::$config[get_class($this)][$type][$relation];
+ if (!isset($options['type'])) {
+ $options = $this->parseRelationOptions($type, $relation, $options, $this->db_table());
+ $options['type'] = $type;
+ self::$config[get_class($this)][$type][$relation] = $options;
+ }
+ break;
+ }
+ }
+ return $options;
+ }
+
+ /**
+ * returns table and columns metadata
+ *
+ * @return array assoc array with columns, primary keys and name of table
+ */
+ function getTableMetadata()
+ {
+ return ['fields' => $this->db_fields(),
+ 'pk' => $this->pk(),
+ 'table' => $this->db_table(),
+ 'additional_fields' => $this->additional_fields(),
+ 'alias_fields' => $this->alias_fields(),
+ 'relations' => array_keys($this->relations)];
+ }
+
+ /**
+ * returns true, if table has an auto_increment column
+ *
+ * @return boolean
+ */
+ function hasAutoIncrementColumn()
+ {
+ return $this->db_fields()[$this->pk()[0]]['extra'] == 'auto_increment';
+ }
+
+ /**
+ * set primary key for entry, combined keys must be passed as array
+ * @param int|string|array $id primary key
+ * @throws InvalidArgumentException if given key is not complete
+ * @return boolean
+ */
+ public function setId($id)
+ {
+ if (!is_array($id)){
+ $id = [$id];
+ }
+ if (count($this->pk()) != count($id)){
+ throw new InvalidArgumentException("Invalid ID, Primary Key {$this->db_table()} is " .join(",",$this->pk()));
+ } else {
+ foreach ($this->pk() as $count => $key){
+ $this->content[$key] = $id[$count];
+ }
+ return true;
+ }
+ }
+
+ /**
+ * returns primary key, multiple keys as array
+ * @return null|string|array current primary key, null if not set
+ */
+ function getId()
+ {
+ if (count($this->pk()) == 1) {
+ return $this->content[$this->pk()[0]] ?? null;
+ } else {
+ $id = [];
+ foreach ($this->pk() as $key) {
+ if (array_key_exists($key, $this->content)) {
+ $id[] = $this->content[$key];
+ }
+ }
+ return (count($this->pk()) == count($id) ? $id : null);
+ }
+ }
+
+ /**
+ * create new unique pk as md5 hash
+ * if pk consists of multiple columns, false is returned
+ * @return boolean|string
+ */
+ function getNewId()
+ {
+ $id = false;
+ if (count($this->pk()) == 1) {
+ do {
+ $id = md5(uniqid($this->db_table(), 1));
+ $db = DBManager::get()->query("SELECT `{$this->pk()[0]}` FROM `{$this->db_table()}` "
+ . "WHERE `{$this->pk()[0]}` = '$id'");
+ } while($db->fetch());
+ }
+ return $id;
+ }
+
+ /**
+ * returns data of table row as assoc array
+ * pass array of fieldnames or ws separated string to limit
+ * fields
+ *
+ * @param null|array|string $only_these_fields limit returned fields
+ * @return array
+ */
+ function toArray($only_these_fields = null)
+ {
+ $ret = [];
+ if (is_string($only_these_fields)) {
+ $only_these_fields = words($only_these_fields);
+ }
+ $fields = array_diff($this->known_slots(), array_keys($this->relations));
+ if (is_array($only_these_fields)) {
+ $only_these_fields = array_filter(array_map(function($s) {
+ return is_string($s) ? strtolower($s) : null;
+ }, $only_these_fields));
+ $fields = array_intersect($only_these_fields, $fields);
+ }
+ foreach ($fields as $field) {
+ $ret[$field] = $this->getValue($field);
+ if ($ret[$field] instanceof StudipArrayObject) {
+ $ret[$field] = $ret[$field]->getArrayCopy();
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * Returns data of table row as assoc array with raw contents like
+ * they are in the database.
+ * Pass array of fieldnames or ws separated string to limit
+ * fields.
+ *
+ * @param null|array|string $only_these_fields
+ * @return array
+ */
+ function toRawArray($only_these_fields = null)
+ {
+ $ret = [];
+ if (is_string($only_these_fields)) {
+ $only_these_fields = words($only_these_fields);
+ }
+ $fields = array_keys($this->db_fields());
+ if (is_array($only_these_fields)) {
+ $only_these_fields = array_filter(array_map(function ($s) {
+ return is_string($s) ? strtolower($s) : null;
+ }, $only_these_fields));
+ $fields = array_intersect($only_these_fields, $fields);
+ }
+ foreach ($fields as $field) {
+ if ($this->content[$field] instanceof I18NString) {
+ $ret[$field] = $this->content[$field]->original();
+ } elseif ($this->content[$field] === null) {
+ $ret[$field] = null;
+ } else {
+ $ret[$field] = (string)$this->content[$field];
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * returns data of table row as assoc array
+ * including related records with a 'has*' relationship
+ * recurses one level without param
+ *
+ * $only_these_fields limits output for relationships in this way:
+ * $only_these_fields = array('field_1',
+ * 'field_2',
+ * 'relation1',
+ * 'relation2' => array('rel2_f1',
+ * 'rel2_f2',
+ * 'rel2_rel11' => array(
+ * rel2_rel1_f1)
+ * )
+ * )
+ * Here all fields of relation1 will be returned.
+ *
+ * @param null|array|string $only_these_fields limit returned fields
+ * @return array
+ */
+ function toArrayRecursive($only_these_fields = null)
+ {
+ if (is_string($only_these_fields)) {
+ $only_these_fields = words($only_these_fields);
+ }
+ if (is_null($only_these_fields)) {
+ $only_these_fields = $this->known_slots();
+ }
+ $ret = $this->toArray($only_these_fields);
+ $relations = [];
+ if (is_array($only_these_fields)) {
+ foreach ($only_these_fields as $key => $value) {
+ if (!is_array($value) &&
+ array_key_exists(strtolower($value), $this->relations)
+ ) {
+ $relations[strtolower($value)] = 0; //not null|array|string to stop recursion
+ }
+ if (array_key_exists(strtolower($key), $this->relations)) {
+ $relations[strtolower($key)] = $value;
+ }
+ }
+ }
+ if (count($relations)) {
+ foreach ($relations as $relation_name => $relation_only_these_fields) {
+ $options = $this->getRelationOptions($relation_name);
+ if ($options['type'] === 'has_one' ||
+ $options['type'] === 'belongs_to') {
+ $ret[$relation_name] =
+ $this->{$relation_name}->
+ toArrayRecursive($relation_only_these_fields);
+ }
+ if ($options['type'] === 'has_many' ||
+ $options['type'] === 'has_and_belongs_to_many') {
+ $ret[$relation_name] =
+ $this->{$relation_name}->
+ sendMessage('toArrayRecursive',
+ [$relation_only_these_fields]);
+ }
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * returns value of a column
+ *
+ * @throws InvalidArgumentException if column could not be found
+ * @throws BadMethodCallException if getter for additional field could not be found
+ * @param string $field
+ * @return null|string|SimpleORMapCollection
+ */
+ public function getValue($field)
+ {
+ $field = strtolower($field);
+
+ // No value defined, throw exception
+ if (!in_array($field, $this->known_slots())) {
+ throw new InvalidArgumentException(static::class . '::'.$field . ' not found.');
+ }
+
+ // Get value by getter
+ if (isset($this->getter_setter_map()[$field]['get'])) {
+ return call_user_func([$this, $this->getter_setter_map()[$field]['get']]);
+ }
+
+ // Get value from content
+ if (array_key_exists($field, $this->content)) {
+ return $this->content[$field];
+ }
+
+ // Get value from relation
+ if (array_key_exists($field, $this->relations)) {
+ $this->initRelation($field);
+ return $this->relations[$field];
+ }
+
+ // Get value from additional_field
+ if (isset($this->additional_fields()[$field]['get'])) {
+ // Getter is defined as a closure
+ if ($this->additional_fields()[$field]['get'] instanceof Closure) {
+ return call_user_func_array($this->additional_fields()[$field]['get'], [$this, $field]);
+ }
+
+ // Getter is defined as a method of this object
+ return call_user_func([$this, $this->additional_fields()[$field]['get']], $field);
+ }
+
+ // No value found, throw exception
+ throw new RuntimeException('No value could be found for ' . static::class . '::' . $field);
+ }
+
+ /**
+ * gets a value from a related object
+ * only possible, if the relation has cardinality 1
+ * e.g. 'has_one' or 'belongs_to'
+ *
+ * @param string $relation name of relation
+ * @param string $field name of column
+ * @throws InvalidArgumentException if no relation with given name is found
+ * @return mixed the value from the related object
+ */
+ function getRelationValue($relation, $field)
+ {
+ $field = strtolower($field);
+ $options = $this->getRelationOptions($relation);
+ if ($options['type'] === 'has_one' || $options['type'] === 'belongs_to') {
+ return $this->{$relation}->{$field} ?? null;
+ } else {
+ throw new InvalidArgumentException('Relation ' . $relation . ' not found or not applicable.');
+ }
+ }
+
+ /**
+ * returns default value for column
+ *
+ * @param string $field name of column
+ * @return mixed the default value
+ */
+ function getDefaultValue($field)
+ {
+ $default_value = null;
+ if (!isset($this->default_values()[$field])) {
+ if (!in_array($field, $this->pk())) {
+ $meta = $this->db_fields()[$field];
+ if (isset($meta['default'])) {
+ $default_value = $meta['default'];
+ } elseif ($meta['null'] == 'NO') {
+ if (strpos($meta['type'], 'text') !== false || strpos($meta['type'], 'char') !== false) {
+ $default_value = '';
+ }
+ if (strpos($meta['type'], 'int') !== false) {
+ $default_value = '0';
+ }
+ }
+ }
+ } else {
+ $default_value = $this->default_values()[$field];
+ }
+ return $default_value;
+ }
+
+ /**
+ * sets value of a column
+ *
+ * @throws InvalidArgumentException if column could not be found
+ * @throws BadMethodCallException if setter for additional field could not be found
+ * @param string $field
+ * @param mixed $value
+ * @return string
+ */
+ function setValue($field, $value)
+ {
+ $field = strtolower($field);
+ $ret = false;
+ if (in_array($field, $this->known_slots())) {
+ if (isset($this->getter_setter_map()[$field]['set'])) {
+ return call_user_func([$this, $this->getter_setter_map()[$field]['set']], $value);
+ }
+ if (array_key_exists($field, $this->content)) {
+ if (array_key_exists($field, $this->serialized_fields())) {
+ $ret = $this->setSerializedValue($field, $value);
+ } elseif ($this->isI18nField($field)) {
+ $ret = $this->setI18nValue($field, $value);
+ } else {
+ $ret = ($this->content[$field] = $value);
+ }
+ } elseif (isset($this->additional_fields()[$field]['set'])) {
+ if ($this->additional_fields()[$field]['set'] instanceof Closure) {
+ return call_user_func_array($this->additional_fields()[$field]['set'], [$this, $field, $value]);
+ } else {
+ return call_user_func([$this, $this->additional_fields()[$field]['set']], $field, $value);
+ }
+ } elseif (array_key_exists($field, $this->relations)) {
+ $options = $this->getRelationOptions($field);
+ if ($options['type'] === 'has_one' || $options['type'] === 'belongs_to') {
+ if (is_a($value, $options['class_name'])) {
+ $this->relations[$field] = $value;
+ if ($options['type'] == 'has_one') {
+ $foreign_key_value = call_user_func($options['assoc_func_params_func'], $this);
+ call_user_func($options['assoc_foreign_key_setter'], $value, $foreign_key_value);
+ } else {
+ $assoc_foreign_key_value = call_user_func($options['assoc_foreign_key_getter'], $value, $this);
+ if ($assoc_foreign_key_value === null) {
+ throw new InvalidArgumentException(sprintf('trying to set belongs_to object of type: %s, but assoc_foreign_key: %s is null', get_class($value), $options['assoc_foreign_key']));
+ }
+ $this->setValue($options['foreign_key'], $assoc_foreign_key_value);
+ }
+ } elseif (
+ $value === null
+ && $this->db_fields()[$options['foreign_key']]['null'] === 'YES'
+ ) {
+ $this->resetRelation($field);
+ $this->setValue($options['foreign_key'], null);
+
+ } else {
+ throw new InvalidArgumentException(sprintf('relation %s expects object of type: %s', $field, $options['class_name']));
+ }
+ }
+ if ($options['type'] == 'has_many' || $options['type'] == 'has_and_belongs_to_many') {
+ if (is_array($value) || $value instanceof Traversable) {
+ $new_ids = [];
+ $old_ids = $this->{$field}->pluck('id');
+ foreach ($value as $current) {
+ if (!is_a($current, $options['class_name'])) {
+ throw new InvalidArgumentException(sprintf('relation %s expects object of type: %s', $field, $options['class_name']));
+ }
+ if ($options['type'] == 'has_many') {
+ $foreign_key_value = call_user_func($options['assoc_func_params_func'], $this);
+ call_user_func($options['assoc_foreign_key_setter'], $current, $foreign_key_value);
+ }
+ if ($current->id !== null) {
+ $new_ids[] = $current->id;
+ $existing = $this->{$field}->find($current->id);
+ if ($existing) {
+ $existing->setData($current);
+ } else {
+ $this->{$field}->append($current);
+ }
+ } else {
+ $this->{$field}->append($current);
+ }
+ }
+ foreach (array_diff($old_ids, $new_ids) as $to_delete) {
+ $this->{$field}->unsetByPK($to_delete);
+ }
+ } else {
+ throw new InvalidArgumentException(sprintf('relation %s expects collection or array of objects of type: %s', $field, $options['class_name']));
+ }
+ }
+ }
+ } else {
+ throw new InvalidArgumentException(get_class($this) . '::'. $field . ' not found.');
+ }
+ return $ret;
+ }
+
+ /**
+ * magic method for dynamic properties
+ *
+ * @throws InvalidArgumentException if column could not be found
+ * @throws BadMethodCallException if getter for additional field could not be found
+ * @param string $field the column or additional field
+ * @return null|string|SimpleORMapCollection
+ */
+ function __get($field)
+ {
+ return $this->getValue($field);
+ }
+ /**
+ * magic method for dynamic properties
+ *
+ * @throws InvalidArgumentException if column could not be found
+ * @throws BadMethodCallException if setter for additional field could not be found
+ * @param string $field
+ * @param string $value
+ * @return string
+ */
+ function __set($field, $value)
+ {
+ return $this->setValue($field, $value);
+ }
+ /**
+ * magic method for dynamic properties
+ *
+ * @param string $field
+ * @return bool
+ */
+ function __isset($field)
+ {
+ $field = strtolower($field);
+ if (in_array($field, $this->known_slots())) {
+ $value = $this->getValue($field);
+ return $value instanceOf SimpleORMapCollection ? (bool)count($value) : !is_null($value);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * ArrayAccess: Check whether the given offset exists.
+ *
+ * @param string $offset
+ */
+ public function offsetExists($offset): bool
+ {
+ return $this->__isset($offset);
+ }
+
+ /**
+ * ArrayAccess: Get the value at the given offset.
+ *
+ * @throws InvalidArgumentException if column could not be found
+ * @throws BadMethodCallException if getter for additional field could not be found
+ * @param string $offset the column or additional field
+ * @return null|string|SimpleORMapCollection
+ */
+ public function offsetGet($offset): mixed
+ {
+ return $this->getValue($offset);
+ }
+
+ /**
+ * ArrayAccess: Set the value at the given offset.
+ *
+ * @throws InvalidArgumentException if column could not be found
+ * @throws BadMethodCallException if setter for additional field could not be found
+ * @param string $offset
+ * @param mixed $value
+ */
+ public function offsetSet($offset, $value): void
+ {
+ $this->setValue($offset, $value);
+ }
+
+ /**
+ * ArrayAccess: unset the value at the given offset (not applicable)
+ *
+ * @param string $offset
+ */
+ public function offsetUnset($offset): void
+ {
+
+ }
+
+ /**
+ * IteratorAggregate
+ */
+ public function getIterator(): ArrayIterator
+ {
+ return new ArrayIterator($this->toArray());
+ }
+
+ /**
+ * Countable
+ */
+ public function count(): int
+ {
+ return count($this->known_slots()) - count($this->relations);
+ }
+
+ /**
+ * check if given column exists in table
+ * @param string $field
+ * @return boolean
+ */
+ function isField($field)
+ {
+ $field = strtolower($field);
+ return isset($this->db_fields()[$field]);
+ }
+
+ /**
+ * check if given relation exists in this class
+ * @param string $field
+ * @return boolean
+ */
+ function isRelation($field)
+ {
+ $field = strtolower($field);
+ return array_key_exists($field, $this->relations);
+ }
+
+ /**
+ * check if given column is additional
+ * @param string $field
+ * @return boolean
+ */
+ function isAdditionalField($field)
+ {
+ $field = strtolower($field);
+ return isset($this->additional_fields()[$field]);
+ }
+
+ /**
+ * check if given column is an alias
+ * @param string $field
+ * @return boolean
+ */
+ function isAliasField($field)
+ {
+ $field = strtolower($field);
+ return isset($this->alias_fields()[$field]);
+ }
+
+ /**
+ * check if given column is a multi-language field
+ * @param string $field
+ * @return boolean
+ */
+ function isI18nField($field)
+ {
+ $field = strtolower($field);
+ return isset($this->i18n_fields()[$field]);
+ }
+
+ /**
+ * set multiple column values
+ * if second param is set, existing data in object will be
+ * discarded and dirty state is cleared,
+ * else new data overrides old data
+ *
+ * @param ?iterable $data assoc array
+ * @param ?boolean $reset existing data in object will be discarded
+ * @return int|bool number of columns changed
+ */
+ function setData($data, $reset = false)
+ {
+ $count = 0;
+ if ($reset) {
+ if ($this->applyCallbacks('before_initialize') === false) {
+ return false;
+ }
+ $this->initializeContent();
+ }
+ if (is_iterable($data)) {
+ foreach($data as $key => $value) {
+ $key = strtolower($key);
+ if (isset($this->db_fields()[$key])
+ || isset($this->alias_fields()[$key])
+ || isset($this->additional_fields()[$key]['set'])
+ ) {
+ $this->setValue($key, $value);
+ ++$count;
+ }
+ }
+ }
+ if ($reset) {
+ $this->applyCallbacks('after_initialize');
+ }
+ return $count;
+ }
+
+ /**
+ * check if object exists in database
+ * @return boolean
+ */
+ function isNew()
+ {
+ return $this->is_new;
+ }
+
+ /**
+ * check if object was deleted
+ *
+ * @return boolean
+ */
+ function isDeleted()
+ {
+ return $this->is_deleted;
+ }
+
+ /**
+ * set object to new state
+ * @param boolean $is_new
+ * @return boolean
+ */
+ function setNew($is_new)
+ {
+ return $this->is_new = $is_new;
+ }
+
+ /**
+ * returns sql clause with current table and pk
+ * @throws UnexpectedValueException if the primary key is incomplete
+ * @return boolean|array<string>
+ */
+ function getWhereQuery()
+ {
+ $where_query = null;
+ $pk_not_set = [];
+ foreach ($this->pk() as $key) {
+ $pk = $this->content_db[$key] ?? $this->content[$key] ?? null;
+ if (isset($pk)) {
+ $where_query[] = "`{$this->db_table()}`.`{$key}` = " . DBManager::get()->quote($pk);
+ } else {
+ $pk_not_set[] = $key;
+ }
+ }
+ if (!$where_query || count($pk_not_set)){
+ if ($this->isNew()) {
+ return false;
+ } else {
+ throw new UnexpectedValueException(sprintf("primary key incomplete: %s must not be null", join(',',$pk_not_set)));
+ }
+ }
+ return $where_query;
+ }
+
+ /**
+ * restore entry from database
+ * @return boolean
+ */
+ function restore()
+ {
+ $where_query = $this->getWhereQuery();
+ $id = $this->getId();
+ if ($where_query) {
+ if ($this->applyCallbacks('before_initialize') === false) {
+ return false;
+ }
+ $this->initializeContent();
+ $query = "SELECT * FROM `{$this->db_table()}` WHERE "
+ . join(" AND ", $where_query);
+ $st = DBManager::get()->prepare($query);
+ $st->execute();
+ $st->setFetchMode(PDO::FETCH_INTO , $this);
+ if ($st->fetch()) {
+ $this->setNew(false);
+ $this->applyCallbacks('after_initialize');
+ return true;
+ }
+ }
+ $this->setData([], true);
+ $this->setNew(true);
+ if (isset($id)) {
+ $this->setId($id);
+ }
+ return false;
+ }
+
+ /**
+ * store entry in database
+ *
+ * @throws UnexpectedValueException if there are forbidden NULL values
+ * @return number|boolean
+ */
+ function store()
+ {
+ // Set id or prepare setting of id
+ if ($this->isNew() && $this->getId() === null) {
+ // Explicitly set id to 0 if auto increment pk is null
+ if ($this->hasAutoIncrementColumn()) {
+ $this->setId(0);
+ } else {
+ $this->setId($this->getNewId());
+ }
+ }
+
+ if ($this->applyCallbacks('before_store') === false) {
+ return false;
+ }
+
+ $ret = 0;
+
+ if (!$this->isDeleted() && ($this->isDirty() || $this->isNew())) {
+ $callback = $this->isNew() ? 'before_create' : 'before_update';
+ if ($this->applyCallbacks($callback) === false) {
+ return false;
+ }
+
+ // Collect i18n contents
+ $i18ncontent = [];
+ foreach (array_keys($this->i18n_fields()) as $field) {
+ if ($this->content[$field] instanceof I18NString) {
+ $i18ncontent[$field] = $this->content[$field];
+ $this->content[$field] = $this->content[$field]->original();
+ $this->content_db[$field] = $this->content_db[$field]->original();
+ }
+ }
+
+ // Create sql data assignment chunks
+ foreach ($this->db_fields() as $field => $meta) {
+ $value = $this->content[$field];
+ if ($field == 'chdate' && !$this->isFieldDirty($field) && $this->isDirty()) {
+ $value = time();
+ }
+ if ($field == 'mkdate') {
+ if ($this->isNew()) {
+ if (!$this->isFieldDirty($field)) {
+ $value = time();
+ }
+ } else {
+ continue;
+ }
+ }
+ if ($value === null && $meta['null'] == 'NO') {
+ throw new UnexpectedValueException($this->db_table() . '.' . $field . ' must not be null.');
+ }
+ if (is_float($value)) {
+ $value = str_replace(',', '.', $value);
+ }
+ $this->content[$field] = $value;
+ $query_part[] = "`$field` = " . DBManager::get()->quote($value) . " ";
+ }
+
+ // Create store query
+ if (!$this->isNew()) {
+ $where_query = $this->getWhereQuery();
+ $query = "UPDATE `{$this->db_table()}` SET "
+ . implode(',', $query_part);
+ $query .= " WHERE " . join(" AND ", $where_query);
+ } else {
+ $query = "INSERT INTO `{$this->db_table()}` SET "
+ . implode(',', $query_part);
+ }
+ $ret = DBManager::get()->exec($query);
+
+ // Retrieve generated id from database if pk is an auto increment
+ // column
+ if ($this->isNew()) {
+ if ($this->hasAutoIncrementColumn() && !$this->getId()) {
+ $this->setId(DBManager::get()->lastInsertId());
+ }
+ }
+
+ // Store i18n contents
+ foreach ($i18ncontent as $field => $one) {
+ $meta = [
+ 'object_id' => $this->getId(),
+ 'table' => $this->db_table(),
+ 'field' => $field
+ ];
+ $one->setMetadata($meta);
+ $one->storeTranslations();
+ if (!$this->content[$field] instanceof I18NString) {
+ $this->content[$field] = $one;
+ $this->content_db[$field] = clone $one;
+ }
+ }
+
+ // Apply callbacks
+ $this->applyCallbacks($this->isNew() ? 'after_create' : 'after_update');
+ }
+ $rel_ret = $this->storeRelations();
+
+ $this->applyCallbacks('after_store');
+
+ if ($ret || $rel_ret) {
+ $this->restore();
+ }
+ return $ret + $rel_ret;
+ }
+
+ /**
+ * sends a store message to all initialized related objects
+ * if a relation has a callback for 'on_store' configured, the callback
+ * is instead invoked
+ *
+ * @param null|array|string $only_these
+ * @return int|false number addition of all return values, false if none was called
+ */
+ protected function storeRelations($only_these = null)
+ {
+ $ret = false;
+ if (is_string($only_these)) {
+ $only_these = words($only_these);
+ }
+ $relations = array_keys($this->relations);
+ if (is_array($only_these)) {
+ $only_these = array_filter(array_map(function ($s) {
+ return is_string($s) ? strtolower($s) : null;
+ }, $only_these));
+ $relations = array_intersect($only_these, $relations);
+ }
+ foreach ($relations as $relation) {
+ $options = $this->getRelationOptions($relation);
+ if (isset($options['on_store']) &&
+ ($options['type'] === 'has_one' ||
+ $options['type'] === 'has_many' ||
+ $options['type'] === 'has_and_belongs_to_many')) {
+ if ($options['on_store'] instanceof Closure) {
+ $ret += call_user_func($options['on_store'], $this, $relation);
+ } elseif (isset($this->relations[$relation])) {
+ $foreign_key_value = call_user_func($options['assoc_func_params_func'], $this);
+ if ($options['type'] === 'has_one') {
+ call_user_func($options['assoc_foreign_key_setter'], $this->{$relation}, $foreign_key_value);
+ $ret = call_user_func([$this->{$relation}, 'store']);
+ } elseif ($options['type'] === 'has_many') {
+ foreach ($this->{$relation} as $r) {
+ call_user_func($options['assoc_foreign_key_setter'], $r, $foreign_key_value);
+ }
+ $ret += array_sum(call_user_func([$this->{$relation}, 'sendMessage'], 'store'));
+ $ret += array_sum(call_user_func([$this->{$relation}->getDeleted(), 'sendMessage'], 'delete'));
+ } else {
+ call_user_func([$this->{$relation}, 'sendMessage'], 'store');
+ $to_delete = array_filter($this->{$relation}->getDeleted()->pluck($options['assoc_foreign_key']));
+ $to_insert = array_filter($this->{$relation}->pluck($options['assoc_foreign_key']));
+ $sql = "DELETE FROM `" . $options['thru_table'] ."` WHERE `" . $options['thru_key'] ."` = ? AND `" . $options['thru_assoc_key'] . "` = ?";
+ $st = DBManager::get()->prepare($sql);
+ foreach ($to_delete as $one_value) {
+ $st->execute([$foreign_key_value, $one_value]);
+ $ret += $st->rowCount();
+ }
+ $sql = "INSERT IGNORE INTO `" . $options['thru_table'] ."` SET `" . $options['thru_key'] ."` = ?, `" . $options['thru_assoc_key'] . "` = ?";
+ $st = DBManager::get()->prepare($sql);
+ foreach ($to_insert as $one_value) {
+ $st->execute([$foreign_key_value, $one_value]);
+ $ret += $st->rowCount();
+ }
+ }
+ }
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * set chdate column to current timestamp
+ * @return boolean
+ */
+ function triggerChdate()
+ {
+ if ($this->db_fields()['chdate']) {
+ $this->content['chdate'] = time();
+ if ($where_query = $this->getWhereQuery()) {
+ DBManager::get()->exec("UPDATE `{$this->db_table()}` SET chdate={$this->content['chdate']}
+ WHERE ". join(" AND ", $where_query));
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * delete entry from database
+ * the object is cleared, but is not(!) turned to new state
+ * @return bool|int number of deleted rows
+ */
+ function delete()
+ {
+ $ret = false;
+ if (!$this->isDeleted() && !$this->isNew()) {
+ if ($this->applyCallbacks('before_delete') === false) {
+ return false;
+ }
+ $ret = $this->deleteRelations();
+ $where_query = $this->getWhereQuery();
+ if ($where_query) {
+ $query = "DELETE FROM `{$this->db_table()}` WHERE "
+ . join(" AND ", $where_query);
+ $ret += DBManager::get()->exec($query);
+ }
+ $this->is_deleted = true;
+ $this->applyCallbacks('after_delete');
+
+ // Remove i18n translations
+ if (I18N::isEnabled()) {
+ foreach (array_keys($this->i18n_fields()) as $field) {
+ if ($this->content[$field] instanceof I18NString) {
+ $this->content[$field]->removeTranslations();
+ }
+ }
+ }
+ }
+ $this->setData([], true);
+ return $ret;
+ }
+
+ /**
+ * sends a delete message to all related objects
+ * if a relation has a callback for 'on_delete' configured, the callback
+ * is invoked instead
+ *
+ * @return bool|int addition of all return values, false if none was called
+ */
+ protected function deleteRelations()
+ {
+ $ret = false;
+ foreach (array_keys($this->relations) as $relation) {
+ $options = $this->getRelationOptions($relation);
+ if (isset($options['on_delete']) &&
+ ($options['type'] === 'has_one' ||
+ $options['type'] === 'has_many' ||
+ $options['type'] === 'has_and_belongs_to_many')) {
+ if ($options['on_delete'] instanceof Closure) {
+ $ret += call_user_func($options['on_delete'], $this, $relation);
+ } else {
+ if ($options['type'] === 'has_one' || $options['type'] === 'has_many') {
+ $this->initRelation($relation);
+ if (isset($this->relations[$relation])) {
+ if ($options['type'] === 'has_one') {
+ $ret += call_user_func([$this->{$relation}, 'delete']);
+ } elseif ($options['type'] === 'has_many') {
+ $ret += array_sum(call_user_func([$this->{$relation}, 'sendMessage'], 'delete'));
+ }
+ }
+ } else {
+ $foreign_key_value = call_user_func($options['assoc_func_params_func'], $this);
+ $sql = "DELETE FROM `" . $options['thru_table'] ."` WHERE `" . $options['thru_key'] ."` = ?";
+ $st = DBManager::get()->prepare($sql);
+ $st->execute([$foreign_key_value]);
+ $ret += $st->rowCount();
+ }
+ }
+ $this->relations[$relation] = null;
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * init internal content arrays with nulls or defaults
+ *
+ * @throws UnexpectedValueException if there is an unmatched alias
+ * @return void
+ */
+ protected function initializeContent()
+ {
+ $this->content = [];
+ foreach (array_keys($this->db_fields()) as $field) {
+ $this->content[$field] = null;
+ $this->content_db[$field] = null;
+ $this->setValue($field, $this->getDefaultValue($field));
+ }
+ foreach ($this->alias_fields() as $alias => $field) {
+ if (isset($this->db_fields()[$field])) {
+ $this->content[$alias] =& $this->content[$field];
+ $this->content_db[$alias] =& $this->content_db[$field];
+ } else {
+ throw new UnexpectedValueException(sprintf('Column %s not found for alias %s', $field, $alias));
+ }
+ }
+ foreach (array_keys($this->relations) as $one) {
+ $this->relations[$one] = null;
+ }
+ $this->additional_data = [];
+ }
+
+ /**
+ * checks if at least one field was modified since last restore
+ *
+ * @return boolean
+ */
+ public function isDirty()
+ {
+ foreach (array_keys($this->db_fields()) as $field) {
+ if ($this->isFieldDirty($field)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * checks if given field was modified since last restore
+ *
+ * @param string $field
+ * @return boolean
+ */
+ public function isFieldDirty($field)
+ {
+ $field = strtolower($field);
+ if ($this->content[$field] === null || $this->content_db[$field] === null) {
+ return $this->content[$field] !== $this->content_db[$field];
+ } else if ($this->content[$field] instanceof I18NString || $this->content_db[$field] instanceof I18NString) {
+ return $this->content[$field] != $this->content_db[$field];
+ } else {
+ return (string)$this->content[$field] !== (string)$this->content_db[$field];
+ }
+ }
+
+ /**
+ * reverts value of given field to last restored value
+ *
+ * @param string $field
+ * @return mixed the restored value
+ */
+ public function revertValue($field)
+ {
+ $field = strtolower($field);
+ return ($this->content[$field] = $this->content_db[$field]);
+ }
+
+ /**
+ * returns unmodified value of given field
+ *
+ * @param string $field
+ * @throws InvalidArgumentException
+ * @return mixed
+ */
+ public function getPristineValue($field)
+ {
+ $field = strtolower($field);
+ if (array_key_exists($field, $this->content_db)) {
+ return $this->content_db[$field];
+ } else {
+ throw new InvalidArgumentException(get_class($this) . '::'. $field . ' not found.');
+ }
+ }
+
+ /**
+ * intitalize a relationship and get related record(s)
+ *
+ * @param string $relation name of relation
+ * @throws InvalidArgumentException if the relation does not exists
+ * @return void
+ */
+ public function initRelation($relation)
+ {
+ if (!array_key_exists($relation, $this->relations)) {
+ throw new InvalidArgumentException('Unknown relation: ' . $relation);
+ }
+ if ($this->relations[$relation] === null) {
+ $options = $this->getRelationOptions($relation);
+ $to_call = [$options['class_name'], $options['assoc_func']];
+ if (!is_callable($to_call)) {
+ throw new RuntimeException('assoc_func: ' . join('::', $to_call) . ' is not callable.' );
+ }
+ $params = $options['assoc_func_params_func'];
+ if ($options['type'] === 'has_many') {
+ $records = function($record) use ($to_call, $params, $options) {
+ $p = (array)$params($record);
+ return call_user_func_array($to_call, array_merge(count($p) ? $p : [null], [$options['order_by'] ?? null]));
+ };
+ $this->relations[$relation] = new SimpleORMapCollection($records, $options, $this);
+ } elseif ($options['type'] === 'has_and_belongs_to_many') {
+ $records = function($record) use ($to_call, $params, $options) {$p = (array)$params($record); return call_user_func_array($to_call, array_merge(count($p) ? $p : [null], [$options]));};
+ $this->relations[$relation] = new SimpleORMapCollection($records, $options, $this);
+ } else {
+ $p = (array)$params($this);
+ $records = call_user_func_array($to_call, count($p) ? $p : [null]);
+ $result = is_array($records) ? ($records[0] ?? null) : $records;
+ $this->relations[$relation] = $result;
+ }
+ }
+ }
+
+ /**
+ * clear data for a relationship
+ *
+ * @param string $relation name of relation
+ * @throws InvalidArgumentException if teh relation does not exists
+ * @return void
+ */
+ public function resetRelation($relation)
+ {
+ if (!array_key_exists($relation, $this->relations)) {
+ throw new InvalidArgumentException('Unknown relation: ' . $relation);
+ }
+ $this->relations[$relation] = null;
+ }
+
+ /**
+ * invoke registered callbacks for given type
+ * if one callback returns false the following will not
+ * be invoked
+ *
+ * @param string $type type of callback
+ * @return bool return value from last callback
+ */
+ protected function applyCallbacks($type)
+ {
+ $ok = true;
+ foreach ($this->registered_callbacks()[$type] as $cb) {
+ if ($cb instanceof Closure) {
+ $function = $cb;
+ $params = [$this, $type, $cb];
+ } else {
+ $function = [$this, $cb];
+ $params = [$type];
+ };
+ $ok = call_user_func_array($function, $params);
+ if ($ok === false) {
+ break;
+ }
+ }
+ return $ok;
+ }
+
+ /**
+ * register given callback for one or many possible callback types
+ * callback param could be a closure or method name of current class
+ *
+ * @param string|array $types types to register callback for
+ * @param callable $cb callback
+ * @throws InvalidArgumentException if the callback type is not known
+ * @return number of registered callbacks
+ */
+ protected static function registerCallback($types, $cb)
+ {
+ trigger_error(__METHOD__ . ' is deprecated. Please use the configuration in configure().', E_USER_DEPRECATED);
+
+ $types = is_array($types) ? $types : words($types);
+ $reg = 0;
+ foreach ($types as $type) {
+ if (isset(static::registered_callbacks()[$type])) {
+ $found = array_search($cb, self::$config[static::class]['registered_callbacks'][$type], true);
+ if ($found === false) {
+ self::$config[static::class]['registered_callbacks'][$type][] = $cb;
+ $reg++;
+ }
+ } else {
+ throw new InvalidArgumentException('Unknown callback type: ' . $type);
+ }
+ }
+ return $reg;
+ }
+
+ /**
+ * unregister given callback for one or many possible callback types
+ *
+ * @param string|array $types types to unregister callback for
+ * @param mixed $cb
+ * @throws InvalidArgumentException if the callback type is not known
+ * @return number of unregistered callbacks
+ */
+ protected static function unregisterCallback($types, $cb)
+ {
+ trigger_error(__METHOD__ . ' is deprecated. Please use the configuration in configure().', E_USER_DEPRECATED);
+
+ $types = is_array($types) ? $types : words($types);
+ foreach ($types as $type) {
+ if (isset(static::registered_callbacks()[$type])) {
+ $found = array_search($cb, self::$config[static::class]['registered_callbacks'][$type], true);
+ if ($found !== false) {
+ $unreg++;
+ unset(self::$config[static::class]['registered_callbacks'][$type][$found]);
+ }
+ } else {
+ throw new InvalidArgumentException('Unknown callback type: ' . $type);
+ }
+ }
+ return $unreg;
+ }
+
+ /**
+ * default callback used to map specific callbacks to NotificationCenter
+ *
+ * @param string $cb_type callback type
+ * @return void|boolean
+ */
+ protected function cbNotificationMapper($cb_type)
+ {
+ if (isset($this->notification_map()[$cb_type])) {
+ try {
+ foreach(words($this->notification_map()[$cb_type]) as $notification) {
+ NotificationCenter::postNotification($notification, $this);
+ }
+ } catch (NotificationVetoException $e) {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * default callback used to map specific callbacks to NotificationCenter
+ *
+ * @param string $cb_type callback type
+ * @return void|boolean
+ */
+ protected function cbAfterInitialize($cb_type)
+ {
+ foreach (array_keys($this->db_fields()) as $field) {
+ if (is_object($this->content[$field])) {
+ $this->content_db[$field] = clone $this->content[$field];
+ } else {
+ $this->content_db[$field] = $this->content[$field];
+ }
+ }
+ }
+
+ /**
+ * default setter used to proxy serialized fields with
+ * ArrayObjects
+ *
+ * @param string $field column name
+ * @param mixed $value value
+ * @return mixed
+ */
+ protected function setSerializedValue($field, $value)
+ {
+ $object_type = $this->serialized_fields()[$field];
+ if (is_null($value) || $value instanceof $object_type) {
+ $this->content[$field] = $value;
+ } else {
+ $this->content[$field] = new $object_type($value);
+ }
+ return $this->content[$field];
+ }
+
+ /**
+ * default setter used to proxy I18N fields with
+ * I18NString
+ *
+ * @param string $field column name
+ * @param mixed $value value
+ * @return mixed
+ */
+ protected function setI18nValue($field, $value)
+ {
+ $meta = ['object_id' => $this->getId(),
+ 'table' => $this->db_table(),
+ 'field' => $field];
+ if ($value instanceof I18NString) {
+ $value->setMetadata($meta);
+ $this->content[$field] = $value;
+ } else {
+ $this->content[$field] = new I18NString($value, null, $meta);
+ }
+ return $this->content[$field];
+ }
+
+ /**
+ * Cleans up this object. This essentially reset all relations of
+ * this object and marks them as unused so that the garbage collector may
+ * remove the objects.
+ *
+ * Use this function when you ran into memory problems and need to free
+ * some memory;
+ *
+ * @return void
+ */
+ public function cleanup()
+ {
+ foreach ($this->relations as $relation => $object) {
+ if ($object instanceof SimpleORMap || $object instanceof SimpleORMapCollection) {
+ $this->relations[$relation]->cleanup();
+ }
+ $this->resetRelation($relation);
+ }
+ }
+}
diff --git a/lib/classes/SimpleORMapCollection.php b/lib/classes/SimpleORMapCollection.php
new file mode 100644
index 0000000..20162e5
--- /dev/null
+++ b/lib/classes/SimpleORMapCollection.php
@@ -0,0 +1,258 @@
+<?php
+/**
+ * SimpleORMapCollection.php
+ * simple object-relational mapping
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @author André Noack <noack@data-quest.de>
+ * @copyright 2012 Stud.IP Core-Group
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ *
+ * @extends SimpleCollection<SimpleORMap>
+ *
+ * @template T of SimpleORMap
+ */
+class SimpleORMapCollection extends SimpleCollection
+{
+ /**
+ * @var int Exception error code denoting a wrong type of objects.
+ */
+ const WRONG_OBJECT_TYPE = 1;
+
+ /**
+ * @var int Exception error code denoting that an object of this `id` already exists.
+ */
+ const OBJECT_EXISTS = 2;
+
+ /**
+ * the record object this collection belongs to
+ *
+ * @var ?SimpleORMap
+ */
+ protected $related_record;
+
+ /**
+ * relation options
+ * @var array
+ */
+ protected $relation_options = [];
+
+ /**
+ * creates a collection from an array of objects
+ * all objects should be of the same type
+ *
+ * @throws InvalidArgumentException if first entry is not SimpleOrMap
+ * @param T[] $data array with SimpleORMap objects
+ * @param bool $strict check every element for correct type and unique pk
+ * @return SimpleORMapCollection<T>
+ */
+ public static function createFromArray(array $data, $strict = true)
+ {
+ $ret = new SimpleORMapCollection();
+ if (count($data)) {
+ $first = current($data);
+ if ($first instanceof SimpleORMap) {
+ $ret->setClassName(get_class($first));
+ if ($strict) {
+ foreach ($data as $one) {
+ $ret[] = $one;
+ }
+ } else {
+ $ret->exchangeArray($data);
+ }
+ } else {
+ throw new InvalidArgumentException('This collection only accepts objects derived from SimpleORMap', self::WRONG_OBJECT_TYPE);
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * Constructor
+ *
+ * @param ?Closure $finder callable to fill collection
+ * @param ?array $options relationship options
+ * @param SimpleORMap|null $record related record
+ */
+ public function __construct(Closure $finder = null, array $options = null, SimpleORMap $record = null)
+ {
+ $this->relation_options = $options;
+ $this->related_record = $record;
+ parent::__construct($finder === null ? [] : $finder);
+ }
+
+ /**
+ * Sets the value at the specified index
+ * checks if the value is an object of specified class
+ *
+ * @see ArrayObject::offsetSet()
+ * @throws InvalidArgumentException if the given model does not fit (wrong type or id)
+ */
+ public function offsetSet($index, $newval): void
+ {
+ if (!is_null($index)) {
+ $index = (int)$index;
+ }
+ if (!is_a($newval, $this->getClassName())) {
+ throw new InvalidArgumentException('This collection only accepts objects of type: ' . $this->getClassName(), self::WRONG_OBJECT_TYPE);
+ }
+ if ($this->related_record && $this->relation_options['type'] === 'has_many') {
+ $foreign_key_value = call_user_func($this->relation_options['assoc_func_params_func'], $this->related_record);
+ call_user_func($this->relation_options['assoc_foreign_key_setter'], $newval, $foreign_key_value);
+ }
+ if ($newval->id !== null) {
+ $exists = $this->find($newval->id);
+ if ($exists) {
+ throw new InvalidArgumentException('Element could not be appended, element with id: ' . $exists->id . ' is in the way', self::OBJECT_EXISTS);
+ }
+ }
+ parent::offsetSet($index, $newval);
+ }
+
+ /**
+ * sets the allowed class name
+ * @param class-string $class_name
+ * @return void
+ */
+ public function setClassName($class_name)
+ {
+ $this->relation_options['class_name'] = strtolower($class_name);
+ $this->deleted->relation_options['class_name'] = strtolower($class_name);
+ }
+
+ /**
+ * sets the related record
+ *
+ * @param SimpleORMap $record
+ * @return void
+ */
+ public function setRelatedRecord(SimpleORMap $record)
+ {
+ $this->related_record = $record;
+ }
+
+ /**
+ * gets the allowed classname
+ *
+ * @return string
+ */
+ public function getClassName()
+ {
+ return strtolower($this->relation_options['class_name']);
+ }
+
+ /**
+ * reloads the elements of the collection
+ * by calling the finder function
+ *
+ * @throws InvalidArgumentException
+ * @return ?int number of records after refresh
+ */
+ public function refresh()
+ {
+ if (is_callable($this->finder)) {
+ $data = call_user_func($this->finder, $this->related_record);
+ foreach ($data as $one) {
+ if (!is_a($one, $this->getClassName())) {
+ throw new InvalidArgumentException('This collection only accepts objects of type: ' . $this->getClassName(), self::WRONG_OBJECT_TYPE);
+ }
+ }
+ $this->exchangeArray($data);
+ $this->deleted->exchangeArray([]);
+ return $this->last_count = $this->count();
+ }
+
+ return null;
+ }
+
+ /**
+ * returns element with given primary key value
+ *
+ * @param string $value primary key value to search for
+ * @return ?T
+ */
+ public function find($value)
+ {
+ return $this->findOneBy('id', $value);
+ }
+
+ /**
+ * returns the collection as grouped array
+ * first param is the column to group by, it becomes the key in
+ * the resulting array, default is pk. Limit returned fields with second param
+ * The grouped entries can optoionally go through the given
+ * callback. If no callback is provided, only the first grouped
+ * entry is returned, suitable for grouping by unique column
+ *
+ * @param string $group_by the column to group by, pk if ommitted
+ * @param mixed $only_these_fields limit returned fields
+ * @param ?callable $group_func closure to aggregate grouped entries
+ * @return array assoc array
+ */
+ public function toGroupedArray($group_by = 'id', $only_these_fields = null, callable $group_func = null)
+ {
+ $result = [];
+ foreach ($this as $record) {
+ $key = $record->getValue($group_by);
+ if (is_array($key)) {
+ $key = join('_', $key);
+ }
+ $result[$key][] = $record->toArray($only_these_fields);
+ }
+ if ($group_func === null) {
+ $group_func = 'current';
+ }
+ return array_map($group_func, $result);
+ }
+
+ /**
+ * mark element(s) for deletion
+ * element(s) with given primary key are moved to
+ * internal deleted collection
+ *
+ * @param string $id primary key of element
+ * @return int number of unsetted elements
+ */
+ public function unsetByPk($id)
+ {
+ return $this->unsetBy('id', $id);
+ }
+
+ /**
+ * merge in another collection, elements must be of
+ * the same type, if an element already exists it is
+ * replaced or ignored depending on second param
+ *
+ * @param SimpleORMapCollection $a_collection
+ * @param string $mode 'replace' or 'ignore'
+ * @return void
+ */
+ public function merge(SimpleCollection $a_collection, string $mode = 'ignore')
+ {
+ $mode = func_get_arg(1);
+ foreach ($a_collection as $element) {
+ try {
+ /**
+ * @throws InvalidArgumentException
+ * @see SimpleORMapCollection::offsetSet()
+ */
+ $this[] = $element;
+ } catch (InvalidArgumentException $e) {
+ if ($e->getCode() === self::OBJECT_EXISTS) {
+ if ($mode === 'replace') {
+ $this->unsetByPk($element->id);
+ $this[] = $element;
+ } // else $mode means 'ignore'
+ } else {
+ throw $e;
+ }
+ }
+ }
+ $this->storage = array_values($this->storage);
+ }
+}
diff --git a/lib/classes/Siteinfo.php b/lib/classes/Siteinfo.php
index 4722b93..4a969ee 100644
--- a/lib/classes/Siteinfo.php
+++ b/lib/classes/Siteinfo.php
@@ -281,7 +281,7 @@ class SiteinfoMarkupEngine {
function __construct() {
$this->db = DBManager::get();
- $this->template_factory = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'].'/app/views/siteinfo/markup/');
+ $this->template_factory = new Flexi\Factory($GLOBALS['STUDIP_BASE_PATH'].'/app/views/siteinfo/markup/');
$this->siteinfoMarkup("/\(:version:\)/", [$this, 'version']);
$this->siteinfoMarkup("/\(:uniname:\)/", [$this, 'uniName']);
$this->siteinfoMarkup("/\(:unicontact:\)/", [$this, 'uniContact']);
@@ -419,7 +419,7 @@ class SiteinfoMarkupEngine {
}
function coregroup() {
- $cache = StudipCacheFactory::getCache();
+ $cache = \Studip\Cache\Factory::getCache();
if (!($remotefile = $cache->read('coregroup'))) {
$remotefile = file_get_contents('https://develop.studip.de/studip/extern.php?module=Persons&config_id=8d1dafc3afca2bce6125d57d4119b631&range_id=4498a5bc62d7974d0a0ac3e97aca5296', false, get_default_http_stream_context('https://develop.studip.de'));
$cache->write('coregroup', $remotefile);
@@ -428,7 +428,7 @@ class SiteinfoMarkupEngine {
}
function toplist($item) {
- $cache = StudipCacheFactory::getCache();
+ $cache = \Studip\Cache\Factory::getCache();
if ($found_in_cache = $cache->read(__METHOD__ . $item)) {
return $found_in_cache;
}
@@ -480,7 +480,7 @@ class SiteinfoMarkupEngine {
// get TopTen of seminars from all ForumModules and add up the
// count for seminars with more than one active ForumModule
// to get a combined toplist
- foreach (PluginEngine::getPlugins('ForumModule') as $plugin) {
+ foreach (PluginEngine::getPlugins(ForumModule::class) as $plugin) {
$new_seminars = $plugin->getTopTenSeminars();
foreach ($new_seminars as $sem) {
if (!isset($seminars[$sem['seminar_id']])) {
@@ -531,7 +531,7 @@ class SiteinfoMarkupEngine {
}
function indicator($key) {
- $cache = StudipCacheFactory::getCache();
+ $cache = \Studip\Cache\Factory::getCache();
if ($found_in_cache = $cache->read(__METHOD__ . $key)) {
return $found_in_cache;
}
@@ -576,11 +576,7 @@ class SiteinfoMarkupEngine {
"title" => _("Fragebögen"),
"detail" => "",
"constraint" => Config::get()->VOTE_ENABLE];
- $indicator['evaluation'] = ["count" => ['count_table_rows','eval'],
- "title" => _("Evaluationen"),
- "detail" => "",
- "constraint" => Config::get()->VOTE_ENABLE];
- $indicator['wiki_pages'] = ["query" => "SELECT COUNT(DISTINCT keyword) AS count FROM wiki",
+ $indicator['wiki_pages'] = ["query" => "SELECT COUNT(*) AS count FROM wiki_pages",
"title" => _("Wiki-Seiten"),
"detail" => "",
"constraint" => Config::get()->WIKI_ENABLE];
@@ -593,7 +589,7 @@ class SiteinfoMarkupEngine {
$count = 0;
// sum up number of postings for all availabe ForumModules
- foreach (PluginEngine::getPlugins('ForumModule') as $plugin) {
+ foreach (PluginEngine::getPlugins(ForumModule::class) as $plugin) {
$count += $plugin->getNumberOfPostings();
}
@@ -663,7 +659,7 @@ function language_filter($input) {
}
function stripforeignlanguage($language, $text) {
- list($primary, $sub) = explode('_',$_SESSION['_language']);
+ [$primary, $sub] = explode('_',$_SESSION['_language']);
if ($language === $primary || $language === $_SESSION['_language']) {
return str_replace('\"', '"', $text);
} else {
diff --git a/lib/classes/StudipArrayObject.class.php b/lib/classes/StudipArrayObject.php
index fe9aad1..da7e66e 100644
--- a/lib/classes/StudipArrayObject.class.php
+++ b/lib/classes/StudipArrayObject.php
@@ -135,6 +135,38 @@ class StudipArrayObject implements IteratorAggregate, ArrayAccess, Serializable,
}
/**
+ * Called when serializing an ArrayObject
+ */
+ public function __serialize(): array
+ {
+ return get_object_vars($this);
+ }
+
+ /**
+ * Called when unserializing an ArrayObject
+ */
+ public function __unserialize(array $data): void
+ {
+ foreach ($data as $k => $v) {
+ switch ($k) {
+ case 'flag':
+ $this->setFlags($v);
+ break;
+ case 'storage':
+ $this->exchangeArray($v);
+ break;
+ case 'iteratorClass':
+ $this->setIteratorClass($v);
+ break;
+ case 'protectedProperties':
+ break;
+ default:
+ $this->__set($k, $v);
+ }
+ }
+ }
+
+ /**
* Appends the value
*
* @param mixed $value
@@ -157,13 +189,8 @@ class StudipArrayObject implements IteratorAggregate, ArrayAccess, Serializable,
/**
* Get the number of public properties in the ArrayObject
- *
- * @return int
- *
- * @todo Add int return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function count()
+ public function count(): int
{
return count($this->storage);
}
@@ -216,13 +243,8 @@ class StudipArrayObject implements IteratorAggregate, ArrayAccess, Serializable,
/**
* Create a new iterator from an ArrayObject instance
- *
- * @return \Iterator
- *
- * @todo Add Traversable return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function getIterator()
+ public function getIterator(): Traversable
{
$class = $this->iteratorClass;
@@ -273,12 +295,8 @@ class StudipArrayObject implements IteratorAggregate, ArrayAccess, Serializable,
* Returns whether the requested key exists
*
* @param mixed $key
- * @return bool
- *
- * @todo Add void return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetExists($key)
+ public function offsetExists($key): bool
{
return isset($this->storage[$key]);
}
@@ -287,12 +305,8 @@ class StudipArrayObject implements IteratorAggregate, ArrayAccess, Serializable,
* Returns the value at the specified key
*
* @param mixed $key
- * @return mixed
- *
- * @todo Add mixed return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetGet($key)
+ public function offsetGet($key): mixed
{
$ret = null;
if (!$this->offsetExists($key)) {
@@ -308,12 +322,8 @@ class StudipArrayObject implements IteratorAggregate, ArrayAccess, Serializable,
*
* @param mixed $key
* @param mixed $value
- * @return void
- *
- * @todo Add void return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetSet($key, $value)
+ public function offsetSet($key, $value): void
{
if (is_null($key)) {
$this->append($value);
@@ -326,12 +336,8 @@ class StudipArrayObject implements IteratorAggregate, ArrayAccess, Serializable,
* Unsets the value at the specified key
*
* @param mixed $key
- * @return void
- *
- * @todo Add void return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetUnset($key)
+ public function offsetUnset($key): void
{
if ($this->offsetExists($key)) {
unset($this->storage[$key]);
diff --git a/lib/classes/StudipAutoloader.php b/lib/classes/StudipAutoloader.php
index 7054478..883adc7 100644
--- a/lib/classes/StudipAutoloader.php
+++ b/lib/classes/StudipAutoloader.php
@@ -52,8 +52,8 @@ class StudipAutoloader
// file is found quickly and unneccessary, costly calls to file_exists()
// can be avoided.
protected static $file_extensions = [
- '.class.php',
'.php',
+ '.class.php',
'.interface.php',
];
diff --git a/lib/classes/StudipCache.class.php b/lib/classes/StudipCache.class.php
deleted file mode 100644
index ba929f9..0000000
--- a/lib/classes/StudipCache.class.php
+++ /dev/null
@@ -1,94 +0,0 @@
-<?php
-/**
- * An interface which has to be implemented by instances returned from
- * StudipCacheFactory#getCache
- *
- * @package studip
- * @subpackage lib
- *
- * @author Marco Diedrich (mdiedric@uos)
- * @author Marcus Lunzenauer (mlunzena@uos.de)
- * @copyright (c) Authors
- * @since 1.6
- * @license GPL2 or any later version
- */
-
-interface StudipCache
-{
- const DEFAULT_EXPIRATION = 12 * 60 * 60; // 12 hours
-
- /**
- * Expire item from the cache.
- *
- * Example:
- *
- * # expires foo
- * $cache->expire('foo');
- *
- * @param string $arg a single key
- */
- public function expire($arg);
-
- /**1
- * Expire all items from the cache.
- */
- public function flush();
-
- /**
- * Retrieve item from the server.
- *
- * Example:
- *
- * # reads foo
- * $foo = $cache->reads('foo');
- *
- * @param string $arg a single key
- *
- * @return mixed the previously stored data if an item with such a key
- * exists on the server or FALSE on failure.
- */
- public function read($arg);
-
- /**
- * Store data at the server.
- *
- * @param string $name the item's key.
- * @param mixed $content the item's content (will be serialized if necessary).
- * @param int $expires the item's expiry time in seconds. Optional, defaults to 12h.
- *
- * @return bool returns TRUE on success or FALSE on failure.
- */
- public function write($name, $content, $expires = self::DEFAULT_EXPIRATION);
-
- /**
- * @return string A translateable display name for this cache class.
- */
- public static function getDisplayName(): string;
-
- /**
- * Get some statistics from cache, like number of entries, hit rate or
- * whatever the underlying cache provides.
- * Results are returned in form of an array like
- * "[
- * [
- * 'name' => <displayable name>
- * 'value' => <value of the current stat>
- * ]
- * ]"
- *
- * @return array
- */
- public function getStats(): array;
-
- /**
- * Return the Vue component name and props that handle configuration.
- * The associative array is of the form
- * [
- * 'component' => <Vue component name>,
- * 'props' => <Properties for component>
- * ]
- *
- * @return array
- */
- public static function getConfig(): array;
-}
diff --git a/lib/classes/StudipCachedArray.php b/lib/classes/StudipCachedArray.php
index 18bb55b..46723f4 100644
--- a/lib/classes/StudipCachedArray.php
+++ b/lib/classes/StudipCachedArray.php
@@ -27,10 +27,10 @@ class StudipCachedArray implements ArrayAccess
* @param int $duration Duration in seconds for which the item shall be
* stored
*/
- public function __construct(string $key, int $duration = StudipCache::DEFAULT_EXPIRATION)
+ public function __construct(string $key, int $duration = \Studip\Cache\Cache::DEFAULT_EXPIRATION)
{
$this->key = self::class . "/{$key}";
- $this->cache = StudipCacheFactory::getCache();
+ $this->cache = \Studip\Cache\Factory::getCache();
$this->duration = $duration;
$this->hash = $this->getHash();
@@ -71,13 +71,8 @@ class StudipCachedArray implements ArrayAccess
* Returns the value at given offset or null if it doesn't exist.
*
* @param string $offset Offset
- *
- * @return mixed
- *
- * @todo Add mixed return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetGet($offset)
+ public function offsetGet($offset): mixed
{
$this->loadData($offset);
return $this->data[$offset];
@@ -121,11 +116,14 @@ class StudipCachedArray implements ArrayAccess
protected function loadData(string $offset)
{
if (!array_key_exists($offset, $this->data)) {
- $cached = $this->cache->read($this->getCacheKey($offset));
- $this->data[$offset] = $this->swapNullAndFalse($cached);
+ // Get the cache item from the cache:
+ $item = $this->cache->getItem($this->getCacheKey($offset));
+ if ($item->isHit()) {
+ $this->data[$offset] = $this->swapNullAndFalse($item->get());
+ }
}
- return $this->data[$offset];
+ return $this->data[$offset] ?? null;
}
/**
@@ -137,13 +135,12 @@ class StudipCachedArray implements ArrayAccess
*/
protected function storeData(string $offset): void
{
- $data = $this->swapNullAndFalse($this->data[$offset]);
-
- $this->cache->write(
+ $item = new \Studip\Cache\Item(
$this->getCacheKey($offset),
- $data,
+ $this->swapNullAndFalse($this->data[$offset]),
$this->duration
);
+ $this->cache->save($item);
}
/**
diff --git a/lib/classes/StudipController.php b/lib/classes/StudipController.php
new file mode 100644
index 0000000..a908a47
--- /dev/null
+++ b/lib/classes/StudipController.php
@@ -0,0 +1,885 @@
+<?php
+/*
+ * StudipController.php - studip controller base class
+ * Copyright (c) 2009 Elmar Ludwig
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ */
+
+use DebugBar\DebugBar;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Writer\Csv;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+/**
+ * @property StudipResponse $response
+ */
+abstract class StudipController extends Trails\Controller
+{
+ use StudipControllerPropertiesTrait;
+
+ protected $with_session = false; //do we need to have a session for this controller
+ protected $allow_nobody = true; //should 'nobody' allowed for this controller or redirected to login?
+ protected $_autobind = false;
+
+ /**
+ * @return false|void
+ */
+ public function before_filter(&$action, &$args)
+ {
+ $this->current_action = $action;
+ // allow only "word" characters in arguments
+ $this->validate_args($args);
+
+ parent::before_filter($action, $args);
+
+ if ($this->with_session) {
+ # open session
+ page_open([
+ 'sess' => 'Seminar_Session',
+ 'auth' => $this->allow_nobody ? 'Seminar_Default_Auth' : 'Seminar_Auth',
+ 'perm' => 'Seminar_Perm',
+ 'user' => 'Seminar_User'
+ ]);
+
+ // show login-screen, if authentication is "nobody"
+ $GLOBALS['auth']->login_if((Request::get('again') || !$this->allow_nobody) && $GLOBALS['user']->id == 'nobody');
+
+ // Setup flash instance
+ $this->flash = Trails\Flash::instance();
+
+ // set up user session
+ include 'lib/seminar_open.php';
+ }
+
+ // Set generic attribute that indicates whether the request was sent
+ // via ajax or not
+ $this->via_ajax = Request::isXhr();
+
+ # Set base layout
+ #
+ # If your controller needs another layout, overwrite your controller's
+ # before filter:
+ #
+ # class YourController extends AuthenticatedController {
+ # function before_filter(&$action, &$args) {
+ # parent::before_filter($action, $args);
+ # $this->set_layout("your_layout");
+ # }
+ # }
+ #
+ # or unset layout by sending:
+ #
+ # $this->set_layout(NULL)
+ #
+ $layout_file = Request::isXhr()
+ ? 'layouts/dialog.php'
+ : 'layouts/base.php';
+ $layout = $GLOBALS['template_factory']->open($layout_file);
+ $this->set_layout($layout);
+
+ $this->set_content_type('text/html;charset=utf-8');
+ }
+
+ /**
+ * Extended method to inject extended response object.
+ */
+ public function erase_response()
+ {
+ parent::erase_response();
+
+ $this->response = new StudipResponse();
+ }
+
+ /**
+ * Hooked perform method in order to inject body element id creation.
+ *
+ * In order to avoid clashes, these body element id will be joined
+ * with a minus sign. Otherwise the controller "x" with action
+ * "y_z" would be given the same id as the controller "x/y" with
+ * the action "z", namely "x_y_z". With the minus sign this will
+ * result in the ids "x-y_z" and "x_y-z".
+ *
+ * Plugins will always have a leading 'plugin-' and the decamelized
+ * plugin name in front of the id.
+ *
+ * @param String $unconsumed_path Path segment containing action and
+ * optionally arguments or format
+ * @return Trails\Response from parent controller
+ */
+ public function perform($unconsumed_path)
+ {
+ // Set body element id if it has not already been set
+ if (!PageLayout::hasBodyElementId()) {
+ $body_id = $this->getBodyElementIdForControllerAndAction($unconsumed_path);
+ PageLayout::setBodyElementId($body_id);
+ }
+
+ return parent::perform($unconsumed_path);
+ }
+
+ /**
+ * Callback function being called after an action is executed.
+ *
+ * @param string Name of the action to perform.
+ * @param array An array of arguments to the action.
+ *
+ * @return void
+ */
+ public function after_filter($action, $args)
+ {
+ parent::after_filter($action, $args);
+
+ if (Request::isXhr() && !isset($this->response->headers['X-Title']) && PageLayout::hasTitle()) {
+ $this->response->add_header('X-Title', rawurlencode(PageLayout::getTitle()));
+ }
+ if (Request::isXhr() && !isset($this->response->headers['X-WikiLink']) && PageLayout::getHelpKeyword()) {
+ $this->response->add_header('X-WikiLink', format_help_url(PageLayout::getHelpKeyword()));
+ }
+
+ if ($this->with_session) {
+ page_close();
+ }
+ }
+
+ /**
+ * Validate arguments based on a list of given types. The types are:
+ * 'int', 'float', 'option' and 'string'. If the list of types is NULL
+ * or shorter than the argument list, 'option' is assumed for all
+ * remaining arguments. 'option' differs from Request::option() in
+ * that it also accepts the charaters '-' and ',' in addition to all
+ * word characters.
+ *
+ * Since Stud.IP 4.0 it is also possible to directly inject
+ * SimpleORMap objects. If types is NULL, the signature of the called
+ * action is analyzed and any type hint that matches a sorm class
+ * will be used to create an object using the argument as the id
+ * that is passed to the object's constructor.
+ *
+ * If $_autobind is set to true, the created object is also assigned
+ * to the controller so that it is available in a view.
+ *
+ * @param array $args an array of arguments to the action
+ * @param array $types list of argument types (optional)
+ */
+ public function validate_args(&$args, $types = null)
+ {
+ $class_infos = [];
+
+ if ($types === null) {
+ $types = [];
+ }
+
+ if ($this->has_action($this->current_action)) {
+ $reflection = new ReflectionMethod($this, $this->current_action . '_action');
+ $parameters = $reflection->getParameters();
+ foreach ($parameters as $i => $parameter) {
+ $class_type = $parameter->getType();
+
+ if (
+ !$class_type
+ || !class_exists($class_type->getName())
+ || !is_a($class_type->getName(), SimpleORMap::class, true)
+ ) {
+ continue;
+ }
+
+ $types[$i] = 'sorm';
+ $class_infos[$i] = [
+ 'model' => $class_type->getName(),
+ 'var' => $parameter->getName(),
+ 'optional' => $parameter->isOptional(),
+ ];
+
+ if ($parameter->isOptional() && !isset($args[$i])) {
+ $args[$i] = $parameter->getDefaultValue();
+ }
+ }
+ }
+
+ foreach ($args as $i => &$arg) {
+ $type = $types[$i] ?? 'option';
+ switch ($type) {
+ case 'int':
+ $arg = (int) $arg;
+ break;
+
+ case 'float':
+ $arg = (float) strtr($arg, ',', '.');
+ break;
+
+ case 'option':
+ if (preg_match('/[^\\w,-]/', $arg)) {
+ throw new Trails\Exception(400);
+ }
+ break;
+
+ case 'sorm':
+ $info = $class_infos[$i];
+
+ $id = null;
+ if ($arg != -1) {
+ $id = $arg;
+ }
+ if (mb_strpos($id, SimpleORMap::ID_SEPARATOR) !== false) {
+ $id = explode(SimpleORMap::ID_SEPARATOR, $id);
+ }
+
+ $reflection = new ReflectionClass($info['model']);
+
+ $sorm = $reflection->newInstance($id);
+ if (!$info['optional'] && $sorm->isNew()) {
+ throw new Trails\Exception(
+ 404,
+ "Parameter {$info['var']} could not be resolved with value {$arg}"
+ );
+ }
+
+ $arg = $sorm;
+ if ($this->_autobind) {
+ $this->{$info['var']} = $arg;
+ }
+ break;
+
+ case 'string':
+ break;
+
+ default:
+ throw new Trails\Exception(500, 'Unknown type "' . $type . '"');
+ }
+ }
+
+ reset($args);
+ }
+
+ /**
+ * Returns a URL to a specified route to your Trails application.
+ * without first parameter the current action is used
+ * if route begins with a / then the current controller ist prepended
+ * if second parameter is an array it is passed to URLHeper
+ *
+ * @param string a string containing a controller and optionally an action
+ * @param string[] optional arguments
+ *
+ * @return string a URL to this route
+ */
+ public function url_for($to = ''/* , ... */)
+ {
+ $args = func_get_args();
+
+ // Try to create route if none given
+ if ($to === '') {
+ $args[0] = isset($this->parent_controller)
+ ? $this->parent_controller->current_action
+ : $this->current_action;
+ return $this->action_url(...$args);
+ }
+
+ // Create url for a specific action
+ // TODO: This seems odd. You kinda specify an absolute path
+ // to receive a relative url. Meh...
+ //
+ // @deprecated Do not use this, please!
+ if ($to[0] === '/') {
+ $args[0] = substr($to, 1);
+ return $this->action_url(...$args);
+ }
+
+ // Check for absolute URL
+ if ($this->isURL($to)) {
+ throw new InvalidArgumentException(__METHOD__ . ' cannot be used with absolute URLs');
+ }
+
+ // Extract fragment (if any)
+ if (strpos($to, '#') !== false) {
+ [$args[0], $fragment] = explode('#', $to);
+ }
+
+ // Extract parameters (if any)
+ $params = [];
+ if (is_array(end($args))) {
+ $params = array_pop($args);
+ }
+
+ // Map any sorm objects to their ids
+ $args = array_map(function ($arg) {
+ if (is_object($arg) && $arg instanceof SimpleORMap) {
+ return $arg->isNew() ? -1 : $arg->id;
+ }
+ return $arg;
+ }, $args);
+
+ $url = parent::url_for(...$args);
+
+ if (isset($fragment)) {
+ $url .= '#' . $fragment;
+ }
+ return URLHelper::getURL($url, $params);
+ }
+
+ /**
+ * Returns an escaped URL to a specified route to your Trails application.
+ * without first parameter the current action is used
+ * if route begins with a / then the current controller ist prepended
+ * if second parameter is an array it is passed to URLHeper
+ *
+ * @param string a string containing a controller and optionally an action
+ * @param strings optional arguments
+ *
+ * @return string a URL to this route
+ */
+ public function link_for($to = ''/* , ... */)
+ {
+ return htmlReady($this->url_for(...func_get_args()));
+ }
+
+ /**
+ * Redirects the user another page. Accepts multiple parameters just like
+ * url_for().
+ *
+ * @param string $to
+ * @see StudipController::url_for()
+ */
+ public function redirect($to)
+ {
+ $to = $this->adjustToArguments(...func_get_args());
+
+ parent::redirect($to);
+ }
+
+ /**
+ * Relocate the user to another location. This is a specialized version
+ * of redirect that differs in two points:
+ *
+ * - relocate() will force the browser to leave the current dialog while
+ * redirect would refresh the dialog's contents
+ * - relocate() accepts all the parameters that url_for() accepts so it's
+ * no longer neccessary to chain url_for() and redirect()
+ *
+ * @param String $to Location to redirect to
+ */
+ public function relocate($to)
+ {
+ $to = $this->adjustToArguments(...func_get_args());
+
+ if (Request::isDialog()) {
+ $this->response->add_header('X-Location', encodeURI($to));
+ $this->render_nothing();
+ } else {
+ parent::redirect($to);
+ }
+ }
+
+ /**
+ * Returns a URL to a specified route to your Trails application, unless
+ * the parameter is already a valid URL (which is returned unchanged).
+ *
+ * If no absolute url or more than one argument is given, url_for() is
+ * used.
+ */
+ private function adjustToArguments(...$args): string
+ {
+ if (count($args) > 1 && $this->isURL($args[0])) {
+ throw new InvalidArgumentException('Method may not be used with a URL and multiple parameters');
+ }
+
+ if (count($args) === 1 && $this->isURL($args[0])) {
+ return $args[0];
+ }
+
+ return $this->url_for(...$args);
+ }
+
+ /**
+ * Returns whether the given parameter is a valid url.
+ *
+ * @param string $to
+ * @return bool
+ */
+ private function isURL(string $to): bool
+ {
+ return preg_match('#^(/|\w+://)#', $to);
+ }
+
+ /**
+ * Exception handler called when the performance of an action raises an
+ * exception.
+ *
+ * @param object the thrown exception
+ */
+ public function rescue($exception)
+ {
+ throw $exception;
+ }
+
+ /**
+ * render given data as json, data is converted to utf-8
+ *
+ * @param mixed $data
+ */
+ public function render_json($data)
+ {
+ $json = json_encode($data);
+
+ $this->set_content_type('application/json;charset=utf-8');
+ $this->response->add_header('Content-Length', strlen($json));
+ $this->render_text($json);
+ }
+
+ /**
+ * Render given data as csv, data is assumed to be utf-8.
+ * The first row of data may contain column titles.
+ *
+ * @param array $data data as two dimensional array
+ * @param string $filename download file name (optional)
+ * @param string $delimiter field delimiter char (optional)
+ * @param string $enclosure field enclosure char (optional)
+ */
+ public function render_csv($data, $filename = null, $delimiter = ';', $enclosure = '"')
+ {
+ $this->set_content_type('text/csv; charset=UTF-8');
+
+ $output = fopen('php://temp', 'rw');
+ fputs($output, "\xEF\xBB\xBF");
+
+ foreach ($data as $row) {
+ fputcsv($output, $row, $delimiter, $enclosure);
+ }
+
+ rewind($output);
+ $csv_data = stream_get_contents($output);
+ fclose($output);
+
+ if (isset($filename)) {
+ $this->response->add_header('Content-Disposition', 'attachment; ' . encode_header_parameter('filename', $filename));
+ }
+
+ $this->response->add_header('Content-Length', strlen($csv_data));
+
+ $this->render_text($csv_data);
+ }
+
+ /**
+ * Renders a pdf file given by a TCPDF/ExportPDF object.
+ *
+ * @param TCPDF $pdf TCPDF object to render
+ * @param string $filename Filename
+ * @param bool $inline Should the pdf be displayed inline (default: no)
+ */
+ protected function render_pdf(TCPDF $pdf, $filename, $inline = false)
+ {
+ $temp_file = $GLOBALS['TMP_PATH'] . '/' . md5(uniqid('pdf-file', true));
+ $pdf->Output($temp_file, 'F');
+
+ $disposition = $inline ? 'inline' : 'attachment';
+
+ $this->render_temporary_file($temp_file, $filename, 'application/pdf', $disposition);
+ }
+
+ /**
+ * Renders a file
+ * @param string $file Path of the file to render
+ * @param string $filename Name of the file displayed to user
+ * (will equal $file when missing)
+ * @param string $content_type Optional content type (will be determined if missing)
+ * @param string $content_disposition Either attachment (default) or inline
+ * @param Closure $callback Optional callback when download has finished
+ * @param int $chunk_size Optional size of chunks to send (default: 256k)
+ */
+ public function render_file(
+ $file,
+ $filename = null,
+ $content_type = null,
+ $content_disposition = 'attachment',
+ Closure $callback = null,
+ $chunk_size = 262144
+ ) {
+ if (!file_exists($file)) {
+ throw new Trails\Exception(404);
+ }
+
+ if (!is_readable($file)) {
+ throw new Trails\Exception(500);
+ }
+
+ if ($content_type === null) {
+ $content_type = get_mime_type($filename ?: $file);
+ }
+
+ if (!in_array($content_type, get_mime_types())) {
+ $content_type = 'application/octet-stream';
+ }
+
+ if ($content_type === 'application/octet-stream') {
+ $content_disposition = 'attachment';
+ }
+
+ $this->set_content_type($content_type);
+ $this->response->add_header(
+ 'Content-Disposition',
+ "{$content_disposition}; " . encode_header_parameter(
+ 'filename',
+ FileManager::cleanFileName($filename ?: basename($file))
+ )
+ );
+ $this->response->add_header('Content-Length', filesize($file));
+ $this->response->add_header('Content-Transfer-Encoding', 'binary');
+ $this->response->add_header('Pragma', 'public');
+ $this->render_text(function () use ($file, $chunk_size, $callback) {
+ $fp = fopen($file, 'rb');
+
+ while (!feof($fp)) {
+ yield fgets($fp, $chunk_size);
+ }
+
+ fclose($fp);
+
+ if ($callback) {
+ $callback($file);
+ }
+ });
+ }
+
+ /**
+ * Renders a temporary file which will be deleted after transmission.
+ * This is just a convenience method so you don't have to write the delete
+ * callback.
+ *
+ * @param string $file Path of the file to render
+ * @param string $filename Name of the file displayed to user
+ * (will equal $file when missing)
+ * @param string $content_type Optional content type (will be determined if missing)
+ * @param string $content_disposition Either attachment (default) or inline
+ * @param Closure $callback Optional callback when download has finished
+ * @param int $chunk_size Optional size of chunks to send (default: 256k)
+ */
+ public function render_temporary_file(
+ $file,
+ $filename = null,
+ $content_type = null,
+ $content_disposition = 'attachment',
+ Closure $callback = null,
+ $chunk_size = 262144
+
+ ) {
+ $delete_callback = function ($file) use ($callback) {
+ unlink($file);
+
+ if ($callback) {
+ $callback($file);
+ }
+ };
+
+ $this->render_file(
+ $file,
+ $filename,
+ $content_type,
+ $content_disposition,
+ $delete_callback,
+ $chunk_size
+ );
+ }
+
+ public function render_form(\Studip\Forms\Form $form)
+ {
+ $this->render_text($form->render());
+ }
+
+ /**
+ * relays current request to another controller and returns the response
+ * the other controller is given all assigned properties, additional parameters are passed
+ * through
+ *
+ * @param string $to_uri a trails route
+ * @return Trails\Response
+ */
+ public function relay($to_uri/* , ... */)
+ {
+ $args = func_get_args();
+ $uri = array_shift($args);
+ [$controller_path, $unconsumed] = '' === $uri ? $this->dispatcher->default_route() : $this->dispatcher->parse($uri);
+
+ $controller = $this->dispatcher->load_controller($controller_path);
+ $assigns = $this->get_assigned_variables();
+ unset($assigns['controller']);
+ foreach ($assigns as $k => $v) {
+ $controller->$k = $v;
+ }
+ $controller->layout = null;
+ $controller->parent_controller = $this;
+ array_unshift($args, $unconsumed);
+ return $controller->perform_relayed(...$args);
+ }
+
+ /**
+ * Relays current request and performs redirect if neccessary.
+ *
+ * @param string $to_uri a trails route
+ * @return Trails\Response
+ *
+ * @see StudipController::relay()
+ */
+ public function relayWithRedirect(...$args): Trails\Response
+ {
+ $response = $this->relay(...$args);
+
+ // If the relayed action should perform a redirect, do so
+ if (isset($response->headers['Location'])) {
+ header("Location: {$response->headers['Location']}");
+ page_close();
+ die;
+ }
+
+ return $response;
+ }
+
+ /**
+ * perform a given action/parameter string from an relayed request
+ * before_filter and after_filter methods are not called
+ *
+ * @see perform
+ * @param string $unconsumed
+ * @return Trails\Response
+ */
+ public function perform_relayed($unconsumed/* , ... */)
+ {
+ $args = func_get_args();
+ $unconsumed = array_shift($args);
+
+ [$action, $extracted_args] = $this->extract_action_and_args($unconsumed);
+ $this->current_action = $action;
+ $args = array_merge($extracted_args, $args);
+ $callable = $this->map_action($action);
+
+ if (is_callable($callable)) {
+ $callable(...$args);
+ } else {
+ $this->does_not_understand($action, $args);
+ }
+
+ if (!$this->performed) {
+ $this->render_action($action);
+ }
+ return $this->response;
+ }
+
+ public function render_template($template_name, $layout = null)
+ {
+ if (Studip\Debug\DebugBar::isActivated()) {
+ $debugbar = app()->get(Debugbar::class);
+ if (!isset($debugbar['trails'])) {
+ $collector = new \Studip\Debug\TrailsCollector($this);
+ $debugbar->addCollector($collector);
+ }
+ }
+
+ parent::render_template($template_name, $layout);
+ }
+
+ /**
+ * Renders a given template and returns the resulting string.
+ *
+ * @param string $template Name of the template file
+ * @param mixed $layout Optional layout
+ * @return string
+ */
+ public function render_template_as_string($template, $layout = null)
+ {
+ $template = $this->get_template_factory()->open($template);
+ $template->set_layout($layout);
+ $template->set_attributes($this->get_assigned_variables());
+ return $template->render();
+ }
+
+ /**
+ * Magic methods that intercepts all unknown method calls.
+ * If a method is called that matches an action on this controller,
+ * an url to that action is generated.
+ *
+ * Basically, this:
+ *
+ * <code>$controller->url_for('foo/bar/baz/' . $param)</code>
+ *
+ * is equal to calling this on the Foo_BarController:
+ *
+ * <code>$controller->baz($param)</code>
+ *
+ * @param String $method Called method name
+ * @param array $arguments Provided arguments
+ * @return string url to the requested action
+ * @throws Trails\Exceptions\UnknownAction if no action matches the method
+ */
+ public function __call($method, $arguments)
+ {
+ $function = 'action_link';
+ if (mb_strpos($method, 'Link') === mb_strlen($method) - 4) {
+ $method = mb_substr($method, 0, -4);
+ } elseif (mb_strpos($method, 'URL') === mb_strlen($method) - 3) {
+ $function = 'action_url';
+ $method = mb_substr($method, 0, -3);
+ }
+
+ if (!$this->has_action($method)) {
+ throw new Trails\Exceptions\UnknownAction("Unknown action '{$method}'");
+ }
+
+ array_unshift($arguments, $method);
+ return call_user_func_array([$this, $function], $arguments);
+ }
+
+ /**
+ * Returns whether this controller has the specificed action.
+ *
+ * @param string $action Name of the action
+ * @return true if action is defined, false otherwise
+ */
+ public function has_action($action)
+ {
+ return method_exists($this, $action . '_action')
+ || ($this->parent_controller
+ && $this->parent_controller->has_action($action));
+ }
+
+ /**
+ * Generates the url for an action on this controller without the
+ * neccessity to provide the full "path" to the action (since it
+ * is implicitely known).
+ *
+ * Basically, this:
+ *
+ * <code>$controller->url_for('foo/bar/baz/' . $param)</code>
+ *
+ * is equal to calling this on the Foo_BarController:
+ *
+ * <code>$controller->action_url('baz/' . $param)</code>
+ *
+ * @param string $action Name of the action
+ * @return string url to the requested action
+ */
+ public function action_url($action)
+ {
+ $arguments = func_get_args();
+ $arguments[0] = $this->controller_path() . '/' . $arguments[0];
+
+ return $this->url_for(...$arguments);
+ }
+
+ /**
+ * Generates the link for an action on this controller without the
+ * neccessity to provide the full "path" to the action (since it
+ * is implicitely known).
+ *
+ * Basically, this:
+ *
+ * <code>$controller->link_for('foo/bar/baz/' . $param)</code>
+ *
+ * is equal to calling this on the Foo_BarController:
+ *
+ * <code>$controller->action_link('baz/' . $param)</code>
+ *
+ * @param string $action Name of the action
+ * @return string to the requested action
+ */
+ public function action_link($action)
+ {
+ return htmlReady($this->action_url(...func_get_args()));
+ }
+
+ /**
+ * Returns the url path to this controller.
+ *
+ * @return string url path to this controller
+ */
+ protected function controller_path()
+ {
+ $class = get_class($this->parent_controller ?? $this);
+ $controller = mb_substr($class, 0, -mb_strlen('Controller'));
+ $controller = strtosnakecase($controller);
+ return preg_replace('/_{2,}/', '/', $controller);
+ }
+
+
+ /**
+ * Validate the datetime according to specific format.
+ *
+ * @param string $datetime the datetime which should be validate
+ * @param string $format the format that the datetime should have by default H:i for time
+ *
+ * @return bool result of validation
+ */
+ public function validate_datetime($datetime, $format = 'H:i')
+ {
+ $dt = DateTime::createFromFormat($format, $datetime);
+ return $dt && $dt->format($format) == date('H:i',strtotime($datetime));
+ }
+
+ /**
+ * Export xlsx and csv files via PhpSpreadsheet
+ *
+ * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
+ */
+ public function render_spreadsheet(
+ array $header,
+ array $data,
+ string $format,
+ string $filename,
+ ?string $filepath = null
+ ): void {
+ $render_to_browser = false;
+ if ($filepath == null) {
+ $render_to_browser = true;
+ $filepath = tempnam($GLOBALS['TMP_PATH'], 'spreadsheet');
+ }
+ $spreadsheet = new Spreadsheet();
+ $activeWorksheet = $spreadsheet->getActiveSheet();
+ $activeWorksheet->fromArray($header);
+ $activeWorksheet->fromArray($data, null, 'A2');
+
+ if ($format === 'xlsx') {
+ $writer = new Xlsx($spreadsheet);
+ } elseif ($format === 'csv') {
+ $writer = new Csv($spreadsheet);
+ } else {
+ throw new Exception("Format {$format} is not supported");
+ }
+
+ $writer->save($filepath);
+
+ if ($render_to_browser) {
+ $this->response->add_header('Cache-Control', 'cache, must-revalidate');
+ $this->render_temporary_file(
+ $filepath,
+ $filename,
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+ );
+ }
+ }
+
+ /**
+ * Creates the body element id for this controller a given action.
+ *
+ * @param string $unconsumed_path Unconsumed path to extract action from
+ * @return string
+ */
+ protected function getBodyElementIdForControllerAndAction($unconsumed_path)
+ {
+ // Extract action from unconsumed path segment
+ [$action] = $this->extract_action_and_args($unconsumed_path);
+
+ // Extract controller name from class name
+ $controller = preg_replace('/Controller$/', '', get_class($this));
+ $controller = Trails\Inflector::underscore($controller);
+
+ // Build main parts of the body element id
+ $body_id_parts = explode('/', $controller);
+ $body_id_parts[] = $action;
+
+ // Create and set body element id
+ $body_id = implode('-', $body_id_parts);
+
+ return $body_id;
+ }
+}
diff --git a/lib/classes/StudipControllerPropertiesTrait.php b/lib/classes/StudipControllerPropertiesTrait.php
new file mode 100644
index 0000000..4e906fa
--- /dev/null
+++ b/lib/classes/StudipControllerPropertiesTrait.php
@@ -0,0 +1,69 @@
+<?php
+/**
+ * This trait manages all variable assignments to the controller and templates.
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @license GPL2 or any later version
+ * @since Stud.IP 5.2
+ */
+trait StudipControllerPropertiesTrait
+{
+ /**
+ * Stores the assigned variables.
+ * @var array
+ */
+ protected $_template_variables = [];
+
+ /**
+ * Returns whether a variable is set.
+ *
+ * @param string $offset
+ * @return bool
+ */
+ public function __isset(string $offset): bool
+ {
+ return isset($this->_template_variables[$offset]);
+ }
+
+ /**
+ * Stores a variable.
+ *
+ * @param string $offset
+ * @param mixed $value
+ */
+ public function __set(string $offset, $value): void
+ {
+ $this->_template_variables[$offset] = $value;
+ }
+
+ /**
+ * Returns a previously set variable.
+ *
+ * @param string $offset
+ * @return mixed
+ */
+ public function &__get(string $offset)
+ {
+ if (!isset($this->_template_variables[$offset])) {
+ $this->_template_variables[$offset] = null;
+ }
+ return $this->_template_variables[$offset];
+ }
+
+ /**
+ * Unsets a previously set variable
+ *
+ * @param string $offset
+ */
+ public function __unset(string $offset): void
+ {
+ unset($this->_template_variables[$offset]);
+ }
+
+ public function get_assigned_variables(): array
+ {
+ $variables = $this->_template_variables;
+ $variables['controller'] = $this;
+ return $variables;
+ }
+}
diff --git a/lib/classes/StudipCoreFormat.php b/lib/classes/StudipCoreFormat.php
index 24c4ddc..7e3358c 100644
--- a/lib/classes/StudipCoreFormat.php
+++ b/lib/classes/StudipCoreFormat.php
@@ -605,11 +605,6 @@ class StudipCoreFormat extends TextFormat
$title
);
- if ($tag === 'audio') {
- $random_id = 'audio-' . mb_substr(md5(uniqid('audio', true)), -8);
- $media = str_replace('<audio ', '<audio id="' . $random_id . '" onerror="STUDIP.Audio.handle(this);" ', $media);
- }
-
if ($link && $tag === "img") {
$media = sprintf('<a href="%s"%s>%s</a>',
$link,
diff --git a/lib/classes/StudipDbCache.class.php b/lib/classes/StudipDbCache.class.php
deleted file mode 100644
index 865825e..0000000
--- a/lib/classes/StudipDbCache.class.php
+++ /dev/null
@@ -1,123 +0,0 @@
-<?php
-/**
- * StudipCache implementation using database table
- *
- * @package studip
- * @subpackage cache
- *
- * @author Elmar Ludwig <elmar.ludwig@uos.de>
- */
-class StudipDbCache implements StudipCache
-{
-
- /**
- * @return string A translateable display name for this cache class.
- */
- public static function getDisplayName(): string
- {
- return _('Datenbank');
- }
-
- /**
- * Expire item from the cache.
- *
- * @param string $arg a single key
- */
- public function expire($arg)
- {
- $db = DBManager::get();
-
- $stmt = $db->prepare('DELETE FROM cache WHERE cache_key = ?');
- $stmt->execute([$arg]);
- }
-
- /**
- * Expire all items from the cache.
- */
- public function flush()
- {
- $db = DBManager::get();
-
- $db->exec('TRUNCATE TABLE cache');
- }
-
- /**
- * Delete all expired items from the cache.
- */
- public function purge()
- {
- $db = DBManager::get();
-
- $stmt = $db->prepare('DELETE FROM cache WHERE expires < ?');
- $stmt->execute([time()]);
- }
-
- /**
- * Retrieve item from the server.
- *
- * @param string $arg a single key
- *
- * @return mixed the previously stored data if an item with such a key
- * exists on the server or FALSE on failure.
- */
- public function read($arg)
- {
- $db = DBManager::get();
-
- $stmt = $db->prepare('SELECT content FROM cache WHERE cache_key = ? AND expires > ?');
- $stmt->execute([$arg, time()]);
- $result = $stmt->fetchColumn();
-
- return $result !== false ? unserialize($result) : false;
- }
-
- /**
- * Store data at the server.
- *
- * @param string $name the item's key.
- * @param mixed $content the item's content (will be serialized if necessary).
- * @param int $expired the item's expiry time in seconds. Optional, defaults to 12h.
- *
- * @return bool returns TRUE on success or FALSE on failure.
- */
- public function write($name, $content, $expires = self::DEFAULT_EXPIRATION)
- {
- $db = DBManager::get();
-
- $stmt = $db->prepare('REPLACE INTO cache VALUES(?, ?, ?)');
- return $stmt->execute([$name, serialize($content), time() + $expires]);
- }
-
- /**
- * Return statistics.
- *
- * @see StudipCache::getStats()
- *
- * @return array|array[]
- */
- public function getStats(): array
- {
- return [
- __CLASS__ => [
- 'name' => _('Anzahl Einträge'),
- 'value' => DBManager::get()->fetchColumn("SELECT COUNT(*) FROM `cache`")
- ]
- ];
- }
-
- /**
- * Return the Vue component name and props that handle configuration.
- *
- * @see StudipCache::getConfig()
- *
- * @return array
- */
- public static function getConfig(): array
- {
- return [
- 'component' => null,
- 'props' => []
- ];
- }
-
-}
diff --git a/lib/classes/StudipDispatcher.php b/lib/classes/StudipDispatcher.php
index af0ea48..a41635a 100644
--- a/lib/classes/StudipDispatcher.php
+++ b/lib/classes/StudipDispatcher.php
@@ -18,29 +18,31 @@ use Psr\Container\ContainerInterface;
/**
* Use this subclass to easily get an Stud.IP specific
- * Trails_Dispatcher.
+ * Trails\Dispatcher.
*
* Example of use:
+ *
* @code
* // deep in the Stud.IP jungle
* $dispatcher = new StudipDispatcher();
* $dispatcher->dispatch($requested_uri);
* @endcode
*/
-class StudipDispatcher extends Trails_Dispatcher {
-
- /**
- * This variable contains the DI-Container.
- * @var ContainerInterface
- */
- protected $container;
+class StudipDispatcher extends Trails\Dispatcher
+{
+ /**
+ * This variable contains the DI-Container.
+ *
+ * @var ContainerInterface
+ */
+ protected $container;
- /**
- * Create a new Trails_Dispatcher with Stud.IP specific parameters
- * for: trails_root is "$STUDIP_BASE_PATH/app", trails_uri is
- * "dispatch.php" and default_controller is "default" (which does
- * not map to anything).
- */
+ /**
+ * Create a new Trails\Dispatcher with Stud.IP specific parameters
+ * for: trails_root is "$STUDIP_BASE_PATH/app", trails_uri is
+ * "dispatch.php" and default_controller is "default" (which does
+ * not map to anything).
+ */
public function __construct(ContainerInterface $container)
{
global $STUDIP_BASE_PATH, $ABSOLUTE_URI_STUDIP;
@@ -58,6 +60,7 @@ class StudipDispatcher extends Trails_Dispatcher {
* exception instead of the standard trails handling.
*
* @param Exception $exception The exception that occured
+ *
* @throws Exception
*/
public function trails_error($exception)
@@ -66,21 +69,22 @@ class StudipDispatcher extends Trails_Dispatcher {
}
/**
- * Loads the controller file for a given controller path and return an
- * instance of that controller. If an error occures, an exception will be
- * thrown.
- *
- * @param string the relative controller path
- *
- * @return TrailsController an instance of that controller
- */
- function load_controller($controller) {
- require_once "{$this->trails_root}/controllers/{$controller}.php";
- $class = Trails_Inflector::camelize($controller) . 'Controller';
- if (!class_exists($class)) {
- throw new Trails_UnknownController("Controller missing: '$class'");
- }
+ * Loads the controller file for a given controller path and return an
+ * instance of that controller. If an error occures, an exception will be
+ * thrown.
+ *
+ * @param string $controller the relative controller path
+ * @return Trails\Controller an instance of that controller
+ * @throws \Trails\Exceptions\UnknownController
+ */
+ public function load_controller($controller)
+ {
+ require_once "{$this->trails_root}/controllers/{$controller}.php";
+ $class = Trails\Inflector::camelize($controller) . 'Controller';
+ if (!class_exists($class)) {
+ throw new Trails\Exceptions\UnknownController("Controller missing: '$class'");
+ }
- return $this->container->make($class, ['dispatcher' => $this]);
- }
+ return $this->container->make($class, ['dispatcher' => $this]);
+ }
}
diff --git a/lib/classes/StudipFileloader.php b/lib/classes/StudipFileloader.php
index f499b68..aab0b31 100644
--- a/lib/classes/StudipFileloader.php
+++ b/lib/classes/StudipFileloader.php
@@ -27,6 +27,12 @@ class StudipFileloader
$_oldVariableNames = array_keys(get_defined_vars());
foreach (preg_split('/ /', $_filename, -1, PREG_SPLIT_NO_EMPTY) as $file) {
+ if (
+ !file_exists($file)
+ && !stream_resolve_include_path($file)
+ ) {
+ throw new Exception('Missing file '. $file);
+ }
include $file;
}
unset($file);
diff --git a/lib/classes/StudipForm.class.php b/lib/classes/StudipForm.php
index 024e2e3..12029df 100644
--- a/lib/classes/StudipForm.class.php
+++ b/lib/classes/StudipForm.php
@@ -5,7 +5,7 @@
# Lifter010: TODO
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// StudipForm.class.php
+// StudipForm.php
// Class to build HTML formular and handle persistence using PhpLib
//
// Copyright (c) 2003 André Noack <noack@data-quest.de>
diff --git a/lib/classes/StudipItem.interface.php b/lib/classes/StudipItem.php
index b575bdc..b575bdc 100644
--- a/lib/classes/StudipItem.interface.php
+++ b/lib/classes/StudipItem.php
diff --git a/lib/classes/StudipKing.class.php b/lib/classes/StudipKing.php
index 2d1f15c..ae5a14e 100644
--- a/lib/classes/StudipKing.class.php
+++ b/lib/classes/StudipKing.php
@@ -63,7 +63,7 @@ class StudipKing {
private static function get_kings()
{
if (self::$kings === null) {
- $cache = StudipCacheFactory::getCache();
+ $cache = \Studip\Cache\Factory::getCache();
# read cache (unserializing a cache miss - FALSE - does not matter)
$kings = unserialize($cache->read(self::CACHE_KEY));
@@ -118,7 +118,7 @@ class StudipKing {
$kings = [];
// sum up postings for all users from all ForumModules available
- foreach (PluginEngine::getPlugins('ForumModule') as $plugin) {
+ foreach (PluginEngine::getPlugins(ForumModule::class) as $plugin) {
$table = $plugin->getEntryTableInfo();
$query = "SELECT user_id AS id, COUNT(*) AS num FROM ". $table['table'] ." GROUP BY user_id";
$new_kings = self::select_kings($query);
diff --git a/lib/classes/StudipLink.class.php b/lib/classes/StudipLink.php
index b4e80ba..b4e80ba 100644
--- a/lib/classes/StudipLink.class.php
+++ b/lib/classes/StudipLink.php
diff --git a/lib/classes/StudipLock.class.php b/lib/classes/StudipLock.php
index 601db8b..ce5752f 100644
--- a/lib/classes/StudipLock.class.php
+++ b/lib/classes/StudipLock.php
@@ -1,6 +1,6 @@
<?php
/**
- * StudipLock.class.php
+ * StudipLock.php
* class with methods to perform cooperative advisory locking
* using the GET_LOCK feature from Mysql
* https://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_get-lock
diff --git a/lib/classes/StudipLog.class.php b/lib/classes/StudipLog.php
index 7a520c9..7a520c9 100644
--- a/lib/classes/StudipLog.class.php
+++ b/lib/classes/StudipLog.php
diff --git a/lib/classes/StudipLvgruppeSelection.class.php b/lib/classes/StudipLvgruppeSelection.php
index a5da9cb..a5da9cb 100644
--- a/lib/classes/StudipLvgruppeSelection.class.php
+++ b/lib/classes/StudipLvgruppeSelection.php
diff --git a/lib/classes/StudipMail.class.php b/lib/classes/StudipMail.php
index db68a11..c517847 100644
--- a/lib/classes/StudipMail.class.php
+++ b/lib/classes/StudipMail.php
@@ -1,6 +1,6 @@
<?php
/**
- * StudipMail.class.php
+ * StudipMail.php
*
* class for constructing and sending emails in Stud.IP
*
@@ -36,6 +36,11 @@ class StudipMail
*/
private $attachments = [];
/**
+ * Array of attachments that are related to the content
+ * @var array
+ */
+ private $related_attachments = [];
+ /**
* @var array
*/
private $sender;
@@ -307,6 +312,17 @@ class StudipMail
return $this;
}
+ public function addRelatedAttachment(string $file_name, string $name, string $type, string $content_id): void
+ {
+ $this->related_attachments[$name] = [
+ 'FileName' => $file_name,
+ 'Name' => $name,
+ 'Content-Type' => $type,
+ 'Disposition' => 'inline',
+ 'Content-ID' => $content_id
+ ];
+ }
+
/**
* @param $name
* @return StudipMail provides fluent interface
@@ -411,16 +427,29 @@ class StudipMail
$transporter->SetMultipleEncodedEmailHeader($type, $recipients);
}
$transporter->SetEncodedHeader('Subject', $this->getSubject());
- if($this->getBodyHtml()){
- $html_part = '';
+ if($this->getBodyHtml()) {
+ $html_part = 0;
$transporter->CreateQuotedPrintableHTMLPart($this->getBodyHtml(), "", $html_part);
$text_part = '';
$text_message = $this->getBodyText();
+
if(!$text_message){
$text_message = _('Diese Nachricht ist im HTML-Format verfasst. Sie benötigen eine E-Mail-Anwendung, die das HTML-Format anzeigen kann.');
}
$transporter->CreateQuotedPrintableTextPart($transporter->WrapText($text_message), "", $text_part);
+
$part = [$text_part, $html_part];
+ if (count($this->related_attachments) > 0) {
+ $relparts = [$html_part];
+ $i = 99;
+ $multipart = 0;
+ foreach ($this->related_attachments as $one) {
+ $transporter->CreateFilePart($one, $i);
+ $relparts[] = $i;
+ }
+ $transporter->CreateRelatedMultipart($relparts, $multipart);
+ $part = [$text_part, $multipart];
+ }
$transporter->AddAlternativeMultipart($part);
} else {
$transporter->AddQuotedPrintableTextPart($this->getBodyText());
diff --git a/lib/classes/StudipMemoryCache.class.php b/lib/classes/StudipMemoryCache.class.php
deleted file mode 100644
index d38385a..0000000
--- a/lib/classes/StudipMemoryCache.class.php
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-/**
- * The php memory implementation of the StudipCache interface.
- *
- * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
- * @license GPL2 or any later version
- * @since Stud.IP 5.0
- */
-class StudipMemoryCache implements StudipCache
-{
- protected $memory_cache = [];
-
- /**
- * Expires just a single key.
- *
- * @param string the key
- */
- public function expire($key)
- {
- unset($this->memory_cache[$key]);
- }
-
- /**
- * Expire all items from the cache.
- */
- public function flush()
- {
- $this->memory_cache = [];
- }
-
- /**
- * Reads just a single key from the cache.
- *
- * @param string the key
- *
- * @return mixed the corresponding value
- */
- public function read($key)
- {
- if (!isset($this->memory_cache[$key])) {
- return false;
- }
- if ($this->memory_cache[$key]['expires'] < time()) {
- $this->expire($key);
- return false;
- }
- return $this->memory_cache[$key]['data'];
- }
-
- /**
- * Store data at the server.
- *
- * @param string the item's key.
- * @param mixed the item's content (will be serialized if necessary).
- * @param int the item's expiry time in seconds. Defaults to 12h.
- *
- * @returns mixed returns TRUE on success or FALSE on failure.
- *
- */
- public function write($name, $content, $expires = self::DEFAULT_EXPIRATION)
- {
- $this->memory_cache[$name] = [
- 'expires' => time() + $expires,
- 'data' => $content,
- ];
-
- return true;
- }
-
- public static function getDisplayName(): string
- {
- return 'Memory cache';
- }
-
- public function getStats(): array
- {
- return [];
- }
-
- public static function getConfig(): array
- {
- return [];
- }
-}
diff --git a/lib/classes/StudipObject.class.php b/lib/classes/StudipObject.php
index 151fcdb..5f10c86 100644
--- a/lib/classes/StudipObject.class.php
+++ b/lib/classes/StudipObject.php
@@ -5,7 +5,7 @@
# Lifter010: TODO
// +--------------------------------------------------------------------------+
// This file is part of Stud.IP
-// StudipObject.class.php
+// StudipObject.php
//
// Class to provide basic properties of an StudipObject in Stud.IP
//
@@ -36,7 +36,7 @@ define ("INSTANCEOF_STUDIPOBJECT", "StudipObject");
/**
- * StudipObject.class.php
+ * StudipObject.php
*
* Class to provide basic properties of an StudipObject in Stud.IP
*
diff --git a/lib/classes/StudipPDO.class.php b/lib/classes/StudipPDO.php
index e77a37f..77046f7 100644
--- a/lib/classes/StudipPDO.class.php
+++ b/lib/classes/StudipPDO.php
@@ -1,6 +1,6 @@
<?php
/**
- * StudipPDO.class.php - Stud.IP PDO class
+ * StudipPDO.php - Stud.IP PDO class
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
@@ -23,7 +23,6 @@ class StudipPDO extends PDO
// Counter for the queries sent to the database
public $query_count = 0;
- public $queries = [];
/**
* Verifies that the given SQL query only contains a single statement.
@@ -42,26 +41,6 @@ class StudipPDO extends PDO
// Count executed queries (this is placed here since this is the only
// method that is executed on every call to the database)
$this->query_count += 1;
-
- if (!empty($GLOBALS['DEBUG_ALL_DB_QUERIES'])) {
- $trace = debug_backtrace();
-
- $classes = [];
- if (isset($trace[2]['class']) && $trace[2]['class'] === 'SimpleORMap') {
- $classes[] = 'sorm';
- }
- if (isset($trace[1]) && $trace[1]['function'] === 'prepare') {
- $classes[] = 'prepared';
- }
-
- $this->queries[] = [
- 'query' => implode("\n", array_filter(array_map('trim', explode("\n", $statement)))),
- 'classes' => implode(' ', $classes),
- 'trace' => $GLOBALS['DEBUG_ALL_DB_QUERIES_WITH_TRACE']
- ? array_slice($trace, 2)
- : null,
- ];
- }
}
/**
@@ -122,23 +101,20 @@ class StudipPDO extends PDO
* Quotes the given value in a form appropriate for the type.
* If no explicit type is given, the value's PHP type is used.
*
- * @param mixed $value PHP value to quote
+ * @param mixed $string PHP value to quote
* @param ?int $type parameter type (e.g. PDO::PARAM_STR)
* @return string|false quoted SQL string
- *
- * @todo Add string|false return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function quote($value, $type = null)
+ public function quote($string, $type = null): false|string
{
if (!isset($type)) {
- if (is_null($value)) {
+ if (is_null($string)) {
$type = PDO::PARAM_NULL;
- } else if (is_bool($value)) {
+ } else if (is_bool($string)) {
$type = PDO::PARAM_BOOL;
- } else if (is_int($value)) {
+ } else if (is_int($string)) {
$type = PDO::PARAM_INT;
- } else if (is_array($value)) {
+ } else if (is_array($string)) {
$type = StudipPDO::PARAM_ARRAY;
} else {
$type = PDO::PARAM_STR;
@@ -149,28 +125,24 @@ class StudipPDO extends PDO
case PDO::PARAM_NULL:
return 'NULL';
case PDO::PARAM_BOOL:
- return $value ? '1' : '0';
+ return $string ? '1' : '0';
case PDO::PARAM_INT:
- return (int) $value;
+ return (int) $string;
case StudipPDO::PARAM_ARRAY:
- return is_array($value) && count($value) ? join(',', array_map([$this, 'quote'], $value)) : 'NULL';
+ return is_array($string) && count($string) ? join(',', array_map([$this, 'quote'], $string)) : 'NULL';
case StudipPDO::PARAM_COLUMN:
- return preg_replace('/\\W/', '', $value);
+ return preg_replace('/\\W/', '', $string);
default:
- return parent::quote($value);
+ return parent::quote($string);
}
}
/**
* Executes an SQL statement and returns the number of affected rows.
*
- * @param string SQL statement
- * @return int|false number of affected rows
- *
- * @todo Add mixed return type when Stud.IP requires PHP8 minimal
+ * @param string $statement SQL statement
*/
- #[ReturnTypeWillChange]
- public function exec($statement)
+ public function exec(string $statement): false|int
{
$this->verify($statement);
return parent::exec($statement);
diff --git a/lib/classes/StudipPDOStatement.php b/lib/classes/StudipPDOStatement.php
index 4a3d069..80a8dc8 100644
--- a/lib/classes/StudipPDOStatement.php
+++ b/lib/classes/StudipPDOStatement.php
@@ -99,11 +99,8 @@ class StudipPDOStatement implements IteratorAggregate
/**
* Forwards all Iterator methods to the actual statement object.
- *
- * @todo Add Traversable return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function getIterator()
+ public function getIterator(): Traversable
{
return $this->stmt;
}
diff --git a/lib/classes/StudipRangeTree.class.php b/lib/classes/StudipRangeTree.php
index cf88d23..5e1aefe 100644
--- a/lib/classes/StudipRangeTree.class.php
+++ b/lib/classes/StudipRangeTree.php
@@ -5,7 +5,7 @@
# Lifter010: TODO
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// StudipRangeTree.class.php
+// StudipRangeTree.php
// Class to handle structure of the "range tree"
//
// Copyright (c) 2002 André Noack <noack@data-quest.de>
diff --git a/lib/classes/StudipRangeTreeView.class.php b/lib/classes/StudipRangeTreeView.php
index 2532e4b..7b23089 100644
--- a/lib/classes/StudipRangeTreeView.class.php
+++ b/lib/classes/StudipRangeTreeView.php
@@ -6,7 +6,7 @@
# Lifter010: TODO
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// StudipRangeTreeView.class.php
+// StudipRangeTreeView.php
// Class to print out the "range tree"
//
// Copyright (c) 2002 André Noack <noack@data-quest.de>
diff --git a/lib/classes/StudipRangeTreeViewAdmin.class.php b/lib/classes/StudipRangeTreeViewAdmin.php
index 0620957..1c5e5a1 100644
--- a/lib/classes/StudipRangeTreeViewAdmin.class.php
+++ b/lib/classes/StudipRangeTreeViewAdmin.php
@@ -6,7 +6,7 @@
# Lifter010: TODO
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// StudipRangeTreeViewAdmin.class.php
+// StudipRangeTreeViewAdmin.php
// Class to print out the "range tree"
//
// Copyright (c) 2002 André Noack <noack@data-quest.de>
diff --git a/lib/classes/StudipResponse.php b/lib/classes/StudipResponse.php
new file mode 100644
index 0000000..a9f1a4c
--- /dev/null
+++ b/lib/classes/StudipResponse.php
@@ -0,0 +1,55 @@
+<?php
+class StudipResponse extends Trails\Response
+{
+ /**
+ * Outputs this response to the client using "echo" and "header".
+ *
+ * This extension allows the body to be a callable and handles generators
+ * by outputting the chunks yielded by the generator.
+ */
+ public function output()
+ {
+ if (isset($this->status)) {
+ $this->send_header(
+ "{$_SERVER['SERVER_PROTOCOL']} {$this->status} {$this->reason}",
+ true,
+ $this->status
+ );
+ }
+
+ // Send headers
+ foreach ($this->headers as $k => $v) {
+ $this->send_header("{$k}: {$v}");
+ }
+
+ // Determine output
+ if (is_callable($this->body)) {
+ $output = call_user_func($this->body);
+ } else {
+ $output = $this->body;
+ }
+
+ if ($output instanceof Generator) {
+ // Clear output buffer
+ while (ob_get_level()) {
+ ob_end_clean();
+ }
+
+ // Ensure generator will run to the end
+ $abort = ignore_user_abort(true);
+
+ // Output chunks yielded by generator
+ foreach ($output as $chunk) {
+ if (!connection_aborted()) {
+ echo $chunk;
+ flush();
+ }
+ }
+
+ // Reset user abort to previous state
+ ignore_user_abort($abort);
+ } else {
+ echo $output;
+ }
+ }
+}
diff --git a/lib/classes/StudipSemRangeTreeViewSimple.class.php b/lib/classes/StudipSemRangeTreeViewSimple.php
index eecdd70..78b5ccf 100644
--- a/lib/classes/StudipSemRangeTreeViewSimple.class.php
+++ b/lib/classes/StudipSemRangeTreeViewSimple.php
@@ -6,7 +6,7 @@
# Lifter010: TODO
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// StudipSemRangeTreeViewSimple.class.php
+// StudipSemRangeTreeViewSimple.php
// Class to print out the seminar tree
//
// Copyright (c) 2003 André Noack <noack@data-quest.de>
diff --git a/lib/classes/StudipSemSearch.class.php b/lib/classes/StudipSemSearch.php
index 8932dd1..6d86b44 100644
--- a/lib/classes/StudipSemSearch.class.php
+++ b/lib/classes/StudipSemSearch.php
@@ -5,7 +5,7 @@
# Lifter010: TODO
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// StudipSemSearchForm.class.php
+// StudipSemSearchForm.php
// Class to build search formular and execute search
//
// Copyright (c) 2003 André Noack <noack@data-quest.de>
diff --git a/lib/classes/StudipSemSearchHelper.class.php b/lib/classes/StudipSemSearchHelper.php
index 29ee5d3..d221869 100644
--- a/lib/classes/StudipSemSearchHelper.class.php
+++ b/lib/classes/StudipSemSearchHelper.php
@@ -5,7 +5,7 @@
# Lifter010: TODO
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// StudipSemSearchHelper.class.php
+// StudipSemSearchHelper.php
//
//
// Copyright (c) 2003 André Noack <noack@data-quest.de>
diff --git a/lib/classes/StudipSemTree.class.php b/lib/classes/StudipSemTree.php
index 70743ad..70743ad 100644
--- a/lib/classes/StudipSemTree.class.php
+++ b/lib/classes/StudipSemTree.php
diff --git a/lib/classes/StudipSemTreeSearch.class.php b/lib/classes/StudipSemTreeSearch.php
index e79a34f..0d8f935 100644
--- a/lib/classes/StudipSemTreeSearch.class.php
+++ b/lib/classes/StudipSemTreeSearch.php
@@ -5,7 +5,7 @@
# Lifter010: TODO
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// StudipSemTreeSearch.class.php
+// StudipSemTreeSearch.php
// Class to build search formular and execute search
//
// Copyright (c) 2003 André Noack <noack@data-quest.de>
diff --git a/lib/classes/StudipSemTreeView.class.php b/lib/classes/StudipSemTreeView.php
index 3e984cc..f2d6fbe 100644
--- a/lib/classes/StudipSemTreeView.class.php
+++ b/lib/classes/StudipSemTreeView.php
@@ -5,7 +5,7 @@
# Lifter010: TODO
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// StudipSemTreeView.class.php
+// StudipSemTreeView.php
// Class to print out the seminar tree
//
// Copyright (c) 2003 André Noack <noack@data-quest.de>
diff --git a/lib/classes/StudipSemTreeViewAdmin.class.php b/lib/classes/StudipSemTreeViewAdmin.php
index edc65c8..59c926a 100644
--- a/lib/classes/StudipSemTreeViewAdmin.class.php
+++ b/lib/classes/StudipSemTreeViewAdmin.php
@@ -7,7 +7,7 @@
# Lifter010: TODO
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// StudipSemTreeViewAdmin.class.php
+// StudipSemTreeViewAdmin.php
// Class to print out the seminar tree in administration mode
//
// Copyright (c) 2003 André Noack <noack@data-quest.de>
diff --git a/lib/classes/StudipSemTreeViewSimple.class.php b/lib/classes/StudipSemTreeViewSimple.php
index e2dba76..239275b 100644
--- a/lib/classes/StudipSemTreeViewSimple.class.php
+++ b/lib/classes/StudipSemTreeViewSimple.php
@@ -6,7 +6,7 @@
# Lifter010: TODO
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// StudipSemTreeViewSimple.class.php
+// StudipSemTreeViewSimple.php
// Class to print out the seminar tree
//
// Copyright (c) 2003 André Noack <noack@data-quest.de>
diff --git a/lib/classes/StudipStudyAreaSelection.class.php b/lib/classes/StudipStudyAreaSelection.php
index ea2ea6f..ea2ea6f 100644
--- a/lib/classes/StudipStudyAreaSelection.class.php
+++ b/lib/classes/StudipStudyAreaSelection.php
diff --git a/lib/classes/StudipTransformFormat.php b/lib/classes/StudipTransformFormat.php
deleted file mode 100644
index 04cf7a3..0000000
--- a/lib/classes/StudipTransformFormat.php
+++ /dev/null
@@ -1,99 +0,0 @@
-<?php
-/**
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author <mlunzena@uos.de
- * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category Stud.IP
- */
-
-/**
- * Format class to transform text before it is saved into the database.
- * @deprecated since Stud.IP 5.3
- */
-class StudipTransformFormat extends TextFormat
-{
- /**
- * list of global Stud.IP transform markup rules
- */
- private static $studip_rules = [
- 'signature' => [
- 'start' => '(?<!~)~~~(?!~)',
- 'callback' => 'StudipTransformFormat::markupSignature'
- ]
- ,'nop' => [
- 'start' => '\[nop\](.*?)\[\/nop\]',
- 'callback' => 'StudipTransformFormat::markupNoFormat'
- ],
- ];
-
- /**
- * Returns the list of global Stud.IP markup rules as an array.
- * Each entry has the following attributes: 'start', 'end' and
- * 'callback'. The rule name is used as the entry's array key.
- *
- * @return array list of all markup rules
- */
- public static function getStudipMarkups()
- {
- return self::$studip_rules;
- }
-
- /**
- * Adds a new markup rule to the global Stud.IP markup set. This can
- * also be used to replace an existing markup rule. The end regular
- * expression is optional (i.e. may be NULL) to indicate that this
- * rule has an empty content model. The callback is called whenever
- * the rule matches and is passed the following arguments:
- *
- * - $markup the markup parser object
- * - $matches match results of preg_match for $start
- * - $contents (parsed) contents of this markup rule
- *
- * @param string $name name of this rule
- * @param string $start start regular expression
- * @param string $end end regular expression (optional)
- * @param callback $callback function generating output of this rule
- */
- public static function addStudipMarkup($name, $start, $end, $callback)
- {
- self::$studip_rules[$name] = compact('start', 'end', 'callback');
- }
-
- /**
- * Removes a markup rule from the global Stud.IP markup set.
- *
- * @param string $name name of the rule
- */
- public static function removeStudipMarkup($name)
- {
- unset(self::$studip_rules[$name]);
- }
-
- /**
- * Initializes a new StudipFormat instance.
- */
- public function __construct()
- {
- parent::__construct(self::getStudipMarkups());
- }
-
- /**
- * Stud.IP markup for signatures
- */
- protected static function markupSignature($markup, $matches)
- {
- return get_fullname();
- }
-
- /**
- * Stud.IP markup for unformatted text
- */
- protected static function markupNoFormat($markup, $matches)
- {
- return '[nop]' . $markup->quote($matches[1]) . '[/nop]';
- }
-}
diff --git a/lib/classes/StudipTreeNodeCachableTrait.php b/lib/classes/StudipTreeNodeCachableTrait.php
index 31823ac..d40cf70 100644
--- a/lib/classes/StudipTreeNodeCachableTrait.php
+++ b/lib/classes/StudipTreeNodeCachableTrait.php
@@ -37,7 +37,7 @@ trait StudipTreeNodeCachableTrait
return $config;
}
- protected function getDescendantIds(): array
+ public function getDescendantIds(): array
{
$cache = self::getDescendantsCacheArray();
diff --git a/lib/classes/StudygroupAvatar.class.php b/lib/classes/StudygroupAvatar.php
index 8e27f8e..8e27f8e 100644
--- a/lib/classes/StudygroupAvatar.class.php
+++ b/lib/classes/StudygroupAvatar.php
diff --git a/lib/classes/StudygroupModel.php b/lib/classes/StudygroupModel.php
index 76899f1..051ada7 100644
--- a/lib/classes/StudygroupModel.php
+++ b/lib/classes/StudygroupModel.php
@@ -532,7 +532,7 @@ class StudygroupModel
if (StudygroupModel::isInvited($user_id, $sem_id)) {
$subject .= ' ' . _('Einladung akzeptiert');
$message = sprintf(
- _("%s hat die Einladung zur Studiengruppe %s akzeptiert. Klicken Sie auf den untenstehenden Link, um direkt zur Studiengruppe zu gelangen.\n\n [Direkt zur Studiengruppe]%s"),
+ _("%s hat die Einladung zur Studiengruppe %s akzeptiert. Klicken Sie auf den folgenden Link, um direkt zur Studiengruppe zu gelangen.\n\n [Direkt zur Studiengruppe]%s"),
get_fullname($user_id),
$sem->getName(),
URLHelper::getlink(
@@ -543,7 +543,7 @@ class StudygroupModel
} else {
$subject .= ' ' . _('Neuer Mitgliedsantrag');
$message = sprintf(
- _("%s möchte der Studiengruppe %s beitreten. Klicken Sie auf den untenstehenden Link, um direkt zur Studiengruppe zu gelangen.\n\n [Direkt zur Studiengruppe]%s"),
+ _("%s möchte der Studiengruppe %s beitreten. Klicken Sie auf den folgenden Link, um direkt zur Studiengruppe zu gelangen.\n\n [Direkt zur Studiengruppe]%s"),
get_fullname($user_id),
$sem->getName(),
URLHelper::getlink(
diff --git a/lib/classes/TreeAbstract.class.php b/lib/classes/TreeAbstract.php
index ccdb6e1..a1413b3 100644
--- a/lib/classes/TreeAbstract.class.php
+++ b/lib/classes/TreeAbstract.php
@@ -5,7 +5,7 @@
# Lifter010: TODO
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// TreeAbstract.class.php
+// TreeAbstract.php
// Abstract Base Class to handle in-memory tree structures
//
// Copyright (c) 2002 André Noack <noack@data-quest.de>
diff --git a/lib/classes/TreeView.class.php b/lib/classes/TreeView.php
index 243a9b9..0bc2810 100644
--- a/lib/classes/TreeView.class.php
+++ b/lib/classes/TreeView.php
@@ -5,8 +5,8 @@
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// TreeView.class.php
-// Class to print out html represantation of a tree object based on TreeAbstract.class.php
+// TreeView.php
+// Class to print out html represantation of a tree object based on TreeAbstract.php
//
// Copyright (c) 2002 André Noack <noack@data-quest.de>
// Suchi & Berg GmbH <info@data-quest.de>
@@ -26,9 +26,7 @@
// +---------------------------------------------------------------------------+
/**
-* Class to print out html represantation of a tree object based on TreeAbstract.class.php
-*
-* Class to print out html represantation of a tree object based on TreeAbstract.class.php
+* Class to print out html represantation of a tree object based on TreeAbstract.php
*
* @access public
* @author André Noack <noack@data-quest.de>
diff --git a/lib/classes/TwilloConnector.php b/lib/classes/TwilloConnector.php
index 7a57583..40ceff8 100644
--- a/lib/classes/TwilloConnector.php
+++ b/lib/classes/TwilloConnector.php
@@ -50,14 +50,14 @@ class TwilloConnector
public static function uploadMaterial(OERMaterial $material, $user_id = null)
{
$user_id || $user_id = User::findCurrent()->id;
- $base = new EduSharingHelperBase(
+ $base = new \EduSharingApiClient\EduSharingHelperBase(
self::$twillo_base_url,
file_get_contents($GLOBALS['STUDIP_BASE_PATH']."/config/twillo-private.key"),
Config::get()->OERCAMPUS_TWILLO_APPID,
self::getHttpProxy()
// 'data-quest-Test'
);
- $authHelper = new EduSharingAuthHelper($base);
+ $authHelper = new \EduSharingApiClient\EduSharingAuthHelper($base);
if (!static::$ticket) {
static::$ticket = $authHelper->getTicketForUser(TwilloConnector::getTwilloUserID($user_id));
}
@@ -261,13 +261,13 @@ class TwilloConnector
{
$user_id || $user_id = User::findCurrent()->id;
- $base = new EduSharingHelperBase(
+ $base = new \EduSharingApiClient\EduSharingHelperBase(
self::$twillo_base_url,
file_get_contents($GLOBALS['STUDIP_BASE_PATH']."/config/twillo-private.key"),
Config::get()->OERCAMPUS_TWILLO_APPID,
self::getHttpProxy()// 'data-quest-Test'
);
- $authHelper = new EduSharingAuthHelper($base);
+ $authHelper = new \EduSharingApiClient\EduSharingAuthHelper($base);
if (!static::$ticket) {
static::$ticket = $authHelper->getTicketForUser(TwilloConnector::getTwilloUserID($user_id));
}
diff --git a/lib/classes/UpdateInformation.class.php b/lib/classes/UpdateInformation.php
index 959ef72..959ef72 100644
--- a/lib/classes/UpdateInformation.class.php
+++ b/lib/classes/UpdateInformation.php
diff --git a/lib/classes/UserConfig.class.php b/lib/classes/UserConfig.php
index 4cb1ff6..413d4c8 100644
--- a/lib/classes/UserConfig.class.php
+++ b/lib/classes/UserConfig.php
@@ -1,6 +1,6 @@
<?php
/**
- * UserConfig.class.php
+ * UserConfig.php
* provides access to user preferences
*
* This program is free software; you can redistribute it and/or
diff --git a/lib/classes/UserDataAdapter.php b/lib/classes/UserDataAdapter.php
index 0a0c3be..5277145 100644
--- a/lib/classes/UserDataAdapter.php
+++ b/lib/classes/UserDataAdapter.php
@@ -26,66 +26,48 @@ class UserDataAdapter implements ArrayAccess, Countable, IteratorAggregate
/**
* ArrayAccess: Check whether the given offset exists.
- *
- * @todo Add bool return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetExists($offset)
+ public function offsetExists($offset): bool
{
return $this->user->offsetExists($this->adaptOffset($offset));
}
/**
* ArrayAccess: Get the value at the given offset.
- *
- * @todo Add mixed return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetGet($offset)
+ public function offsetGet($offset): mixed
{
return $this->user->offsetGet($this->adaptOffset($offset));
}
/**
* ArrayAccess: Set the value at the given offset.
- *
- * @todo Add void return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetSet($offset, $value)
+ public function offsetSet($offset, $value): void
{
$this->user->offsetSet($this->adaptOffset($offset), $value);
}
/**
* ArrayAccess: unset the value at the given offset.
- *
- * @todo Add void return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function offsetUnset($offset)
+ public function offsetUnset($offset): void
{
$this->user->offsetUnset($this->adaptOffset($offset));
}
/**
* @see Countable::count()
- *
- * @todo Add int return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function count()
+ public function count(): int
{
return $this->user->count();
}
/**
* @see IteratorAggregate::getIterator()
- *
- * @todo Add Traversable return type when Stud.IP requires PHP8 minimal
*/
- #[ReturnTypeWillChange]
- public function getIterator()
+ public function getIterator(): Traversable
{
return $this->user->getIterator();
}
diff --git a/lib/classes/UserLookup.class.php b/lib/classes/UserLookup.php
index ddf9276..62de8fd 100644
--- a/lib/classes/UserLookup.class.php
+++ b/lib/classes/UserLookup.php
@@ -1,6 +1,6 @@
<?php
/**
- * UserLookup.class.php
+ * UserLookup.php
*
* provides an easy way to look up user ids by certain filter criteria
*
@@ -239,7 +239,7 @@ class UserLookup
return call_user_func(self::$types[$type]['values']);
}
- $cache = StudipCacheFactory::getCache();
+ $cache = \Studip\Cache\Factory::getCache();
$cache_key = "UserLookup/{$type}/values";
$cached_values = $cache->read($cache_key);
if ($cached_values) {
diff --git a/lib/classes/UserManagement.class.php b/lib/classes/UserManagement.php
index c831b85..4bd47ec 100644
--- a/lib/classes/UserManagement.class.php
+++ b/lib/classes/UserManagement.php
@@ -1,7 +1,7 @@
<?php
# Lifter007: TODO
/**
- * UserManagement.class.php
+ * UserManagement.php
*
* Management for the Stud.IP global users
*
@@ -25,7 +25,7 @@ require_once 'lib/messaging.inc.php'; // remove messages send or recieved by u
require_once 'lib/object.inc.php';
/**
- * UserManagement.class.php
+ * UserManagement.php
*
* Management for the Stud.IP global users
*
@@ -898,7 +898,7 @@ class UserManagement
// Load privacy plugins to ensure all event handlers can react to the
// UserDataDidRemove event
- PluginEngine::getPlugins('PrivacyPlugin');
+ PluginEngine::getPlugins(PrivacyPlugin::class);
// delete user from instituts
$this->logInstUserDel($this->user_data['auth_user_md5.user_id']);
@@ -1122,18 +1122,6 @@ class UserManagement
// delete the datafields
$localEntries = DataFieldEntry::removeAll($user_id);
- // delete all blubber entrys
- $query = "DELETE blubber_threads, blubber_mentions, blubber_comments
- FROM blubber_threads
- LEFT JOIN blubber_mentions USING (user_id)
- LEFT JOIN blubber_comments USING (user_id)
- WHERE user_id = ?";
- $statement = DBManager::get()->prepare($query);
- $statement->execute([$user_id]);
- if ($count = $statement->rowCount()) {
- $msg .= 'info§' . sprintf(_('%s Blubber gelöscht.'), $count) . '§';
- }
-
// delete user from waiting lists
$query = "SELECT seminar_id FROM admission_seminar_user WHERE user_id = ?";
$statement = DBManager::get()->prepare($query);
@@ -1221,8 +1209,6 @@ class UserManagement
"DELETE FROM priorities WHERE user_id = ?",
"DELETE FROM api_oauth_user_mapping WHERE user_id = ?",
"DELETE FROM api_user_permissions WHERE user_id = ?",
- "DELETE FROM eval_user WHERE user_id = ?",
- "DELETE FROM evalanswer_user WHERE user_id = ?",
"DELETE FROM help_tour_user WHERE user_id = ?",
"DELETE FROM personal_notifications_user WHERE user_id = ?",
"DELETE FROM forum_abo_users WHERE user_id = ?",
diff --git a/lib/classes/Visibility.php b/lib/classes/Visibility.php
index 1b87230..99b3907 100644
--- a/lib/classes/Visibility.php
+++ b/lib/classes/Visibility.php
@@ -614,7 +614,7 @@ class Visibility
private function createHomepagePluginEntries($user)
{
self::getUser($user);
- $homepageplugins = PluginEngine::getPlugins('HomepagePlugin');
+ $homepageplugins = PluginEngine::getPlugins(HomepagePlugin::class);
foreach ($homepageplugins as $plugin) {
self::addPrivacySetting($plugin->getPluginName(), ("plugin".$plugin->getPluginId()), 'plugins', 1, $user, null, $plugin->getPluginId());
}
diff --git a/lib/classes/WidgetHelper.php b/lib/classes/WidgetHelper.php
deleted file mode 100644
index ba82ee9..0000000
--- a/lib/classes/WidgetHelper.php
+++ /dev/null
@@ -1,402 +0,0 @@
-<?php
-/**
- * WidgetHelper.php - utility functions for Widget-Parameter Handling
- * @deprecated since Stud.IP 5.5
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author Nadine Werner <nadine.werner@uni-osnabrueck.de>
- * @author André Klaßen <klassen@elan-ev.de>
- * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category Stud.IP
- * @package index
- * @since 3.1
- */
-
-class WidgetHelper
-{
- /**
- * array of submitted widget parameter values
- */
- private static $params = [];
-
- /**
- * array of submitted widget parameter values
- */
- private static $activeWidget;
-
- /**
- * Saves the widget data of a user
- */
- private static $userWidgets = [];
-
- /**
- * Set the last active Widget
- * @param string $activeWidget
- */
- public static function setActiveWidget($activeWidget)
- {
- self::$activeWidget = $activeWidget;
- }
-
- /**
- * Returns the position in the two column layout on the Startpage
- * If no position is stored in UserConfig, the widget will be displayed on the right side.
- *
- * @param string $pluginid
- *
- * @return the position as array matrix
- */
- public static function getWidgetPosition($pluginid)
- {
- $query = "SELECT position FROM widget_user where id = ?";
- $statement = DBManager::get()->prepare($query);
- $statement->execute([$pluginid]);
- $pos = $statement->fetchColumn();
-
- return $pos;
- }
-
- /**
- * storeNewPositions - stores new Widget positions for a given user
- *
- * @param array $lanes array with column as index and ids array as value
- *
- * @return void
- */
- public static function storeNewPositions(array $lanes): void
- {
- // Query not displayed widgets to sort them to the bottom of a lane
- $query = "SELECT `col`, `id`
- FROM `widget_user`
- WHERE `range_id` = ?
- AND `id` NOT IN (?)
- ORDER BY `col`, `position`";
- $undisplayed = DBManager::get()->fetchGrouped($query, [
- User::findCurrent()->id,
- array_merge(...$lanes)
- ], function ($row) {
- return array_column($row, 'id');
- });
-
- // Set new positions
- $query = "UPDATE `widget_user`
- SET `col` = :column,
- `position` = :position
- WHERE `id` = :id
- AND `range_id` = :user_id";
- $statement = DBManager::get()->prepare($query);
- $statement->bindValue(':user_id', User::findCurrent()->id);
-
- foreach ([0, 1] as $column) {
- $statement->bindValue(':column', $column);
-
- $ids = array_merge(
- $lanes[$column] ?? [],
- $undisplayed[$column] ?? []
- );
-
- $position = 0;
- foreach ($ids as $id) {
- $statement->bindValue(':position', $position++);
- $statement->bindValue(':id', $id);
- $statement->execute();
- }
- }
- }
-
- /**
- * addInitialPositons - adds the global widget default settings to an user setting
- *
- * @param string $col
- * @param array $ids of widgets
- * @param string $range_id
- *
- * @return void
- */
- public static function addInitialPositions($col, $ids, $range_id)
- {
- if(is_array($ids)) {
- foreach ($ids as $pos => $id) {
- $pos = intVal($pos);
- $query = "REPLACE INTO widget_user (`pluginid`, `position`, `range_id`) VALUES (?,?,?);";
- $statement = DBManager::get()->prepare($query);
- $statement->execute([$id, $pos, $range_id]);
- }
- }
- }
-
- /**
- * storeInitialPositions - stores the global widget default for a given perm
- *
- * @param string $col
- * @param array $ids of widgets
- * @param string $perm
- *
- * @return boolean success
- */
- public static function storeInitialPositions($col, $ids, $perm)
- {
- $stmt = DBManager::get()->prepare('DELETE FROM widget_default WHERE `perm` = ? AND `col` = ?;');
- $stmt->execute([$perm, $col]);
-
- if (is_array($ids)) {
- foreach ($ids as $id => $pos) {
- if ($id) {
- $pos = intVal($pos);
- $stmt = DBManager::get()->prepare("REPLACE INTO widget_default (`pluginid`,`col`, `position`, `perm`) VALUES (?,?,?,?);");
- $stmt->execute([$id, $col, $pos, $perm]);
- }
- }
-
- return true;
- }
-
- return false;
- }
-
- public static function getInitialPositions($perm)
- {
- return DBManager::get()->fetchGroupedPairs("SELECT col, pluginid, position FROM widget_default "
- . "WHERE perm = ? "
- . "ORDER BY col ASC, position ASC", [$perm]);
- }
-
- /**
- * Sets the current setting of a user as the default for a usergroup
- *
- * @param string $range_id The range id of the user that defines the setting
- * @param string $group The usergroup
- */
- public static function setAsInitialPositions($range_id, $group)
- {
- DBManager::get()->execute("DELETE FROM widget_default WHERE `perm` = ?", [$group]);
- DBManager::get()->execute('INSERT INTO widget_default (SELECT pluginid, col, position, ? as perm FROM widget_user WHERE range_id = ?)', [$group, $range_id]);
- }
-
- /**
- * setInitialPositions - copies the default to the logged on user
- */
- public static function setInitialPositions()
- {
- $query = "INSERT INTO widget_user (pluginid, position, range_id, col)
- SELECT pluginid, position, :user_id, col
- AS perm
- FROM widget_default
- WHERE perm = :perm
-
- UNION
-
- -- Dummy entry to allow no widgets
- SELECT -1, 0, :user_id, 2";
- DBManager::get()->execute($query, [
- ':user_id' => $GLOBALS['user']->id,
- ':perm' => $GLOBALS['perm']->get_perm(),
- ]);
- }
-
- /**
- * getUserWidgets - retrieves the widget settings for a given user
- *
- * @param string $id
- *
- * @return array $widgets
- */
- public static function getUserWidgets($id, $col = 0)
- {
- $plugin_manager = PluginManager::getInstance();
- $query = "SELECT * FROM widget_user WHERE range_id=? AND col = ? ORDER BY position";
- $statement = DBManager::get()->prepare($query);
- $statement->execute([$id, $col]);
- $widgets = [];
- while ($db_widget = $statement->fetch(PDO::FETCH_ASSOC)) {
- if(!is_null($plugin_manager->getPluginById($db_widget['pluginid']))){
- $widget = clone $plugin_manager->getPluginById($db_widget['pluginid']);
- $widget->widget_id = $db_widget['id'];
- $widgets[$db_widget['position']] = $widget;
- }
- }
- return $widgets;
- }
-
- /**
- * Returns whether a user has any defined widgets.
- * @param string $user_id User id
- * @return boolean
- */
- public static function hasUserWidgets($user_id)
- {
- $query = "SELECT 1 FROM `widget_user` WHERE `range_id` = ?";
- return (bool) DBManager::get()->fetchColumn($query, [$user_id]);
- }
-
- /**
- * addWidgetUserConfig - creates user_config entry for widget newly added by a user
- *
- * @param string $id - user_id
- * @param string $pluginName
- * @param array $confArray
- *
- * @return void
- */
- public static function addWidgetUserConfig($id, $pluginName, $confArray )
- {
- UserConfig::get($id)->store($pluginName, $confArray );
- }
-
-
- /**
- * getWidgetUserConfig - retrieves user_config entry for widget newly added by a user
- *
- * @param string $id user_id
- * @param string $pluginName
- *
- * @return object UserConfig
- */
- public static function getWidgetUserConfig($id, $pluginName)
- {
- return UserConfig::get($id)->getValue($pluginName);
-
- }
-
- /**
- * removeWidget - removes a widget for a user
- *
- * @param string $id - widget_id
- * @param string $pluginName
- * @param string $range_id e.g. user_id
- *
- * @return bool success
- */
- public static function removeWidget($id, $pluginName, $range_id)
- {
- UserConfig::get($range_id)->delete($pluginName);
-
- $query = "DELETE FROM widget_user WHERE id = ? AND range_id = ?";
- $statement = DBManager::get()->prepare($query);
-
- return $statement->execute([$id, $range_id]);
- }
-
- /**
- * addWidget - adds a widget for a given user
- *
- * @param string $id - widget_id
- * @param string $range_id e.g. user_id
- *
- * @return bool|int false on error, id of inserted widget otherwise
- */
- public static function addWidget($id, $range_id)
- {
- $db = DBManager::get();
- $statement = $db->prepare('SELECT MAX(position) + 1 FROM widget_user WHERE range_id = :range_id');
- $statement->bindValue(':range_id', $range_id);
- $statement->execute();
- $position = $statement->fetchColumn() ?: 0;
-
- $statement = $db->prepare('INSERT INTO widget_user (pluginid, position, range_id) VALUES (:id, :position, :range_id)');
- $statement->bindValue(':id', $id);
- $statement->bindValue(':position', $position);
- $statement->bindValue(':range_id', $range_id);
- $result = $statement->execute();
-
- return $result ? $db->lastInsertId() : false;
- }
-
- /**
- * getWidgetName - retrieves the name of a given widget
- *
- * @param string $id - widget_id
- *
- * @return string widget_name
- */
- public static function getWidgetName($id)
- {
- $query = "SELECT `pluginid` FROM `widget_user` WHERE `id`=?";
- $statement = DBManager::get()->prepare($query);
- $statement->execute([$id]);
- $pid = $statement->fetch(PDO::FETCH_ASSOC);
-
- $plugin_manager = PluginManager::getInstance();
- $plugin_info = $plugin_manager->getPluginById($pid['pluginid']);
- return $plugin_info ? $plugin_info->getPluginName() : false;
-
- }
-
-
- /**
- * getWidget - retrieves an instance of a given widget / portal plugin
- *
- * @param string $pluginid
- *
- * @return object widget
- */
- public static function getWidget($pluginid)
- {
- return PluginManager::getInstance()->getPluginById($pluginid);
- }
-
- /**
- * getAvailableWidgets - fetches all widgets that are not already in use.
- *
- * @param string $user_id the user to check
- *
- * @return array All available widgets.
- */
- public static function getAvailableWidgets($user_id = null)
- {
- $all_widgets = PluginEngine::getPlugins('PortalPlugin');
-
- $used_widgets = is_null($user_id)
- ? []
- : DBManager::get()->fetchFirst("SELECT `pluginid` FROM `widget_user` WHERE `range_id`=? ORDER BY `pluginid`", [$user_id]);
-
- $available = [];
- foreach ($all_widgets as $widget) {
- if (!in_array($widget->getPluginId(), $used_widgets)) {
- $available[$widget->getPluginId()] = $widget;
- }
- }
- return $available;
- }
-
- /**
- * hasWidget - returns whether has a certain widget activated
- *
- * @param string $user_id Id of the user
- * @param mixed $widget Id or name of the widget (you may omit the
- * 'Widget' in the name)
- * @return bool indicating whether the widget is activated
- */
- public static function hasWidget($user_id, $widget)
- {
- if (!isset(self::$userWidgets[$user_id])) {
- $statement = DBManager::get()->prepare("
- SELECT *
- FROM widget_user
- WHERE range_id = :user_id
- ");
- $statement->execute(['user_id' => $user_id]);
- self::$userWidgets[$user_id] = $statement->fetchAll(PDO::FETCH_ASSOC);
- }
-
- if (!ctype_digit($widget)) {
- $plugin = PluginManager::getInstance()->getPlugin($widget) ?: PluginManager::getInstance()->getPlugin($widget . "Widget");
- if ($plugin) {
- $widget = $plugin->getPluginId();
- } else {
- return false;
- }
- }
-
- foreach (self::$userWidgets[$user_id] as $widget_user) {
- if ($widget_user['pluginid'] == $widget) {
- return true;
- }
- }
- return false;
- }
-}
diff --git a/lib/classes/admission/AdmissionAlgorithm.class.php b/lib/classes/admission/AdmissionAlgorithm.php
index 7e9df92..fae1061 100644
--- a/lib/classes/admission/AdmissionAlgorithm.class.php
+++ b/lib/classes/admission/AdmissionAlgorithm.php
@@ -1,7 +1,7 @@
<?php
/**
- * AdmissionAlgorithm.class.php
+ * AdmissionAlgorithm.php
*
* Abstract class for seminar seat distribution. A concrete algorithm
* needs to be implemented.
@@ -32,4 +32,4 @@ abstract class AdmissionAlgorithm
} /* end of class AdmissionAlgorithm */
-?> \ No newline at end of file
+?>
diff --git a/lib/classes/admission/AdmissionPriority.class.php b/lib/classes/admission/AdmissionPriority.php
index 47e1564..22d426e 100644
--- a/lib/classes/admission/AdmissionPriority.class.php
+++ b/lib/classes/admission/AdmissionPriority.php
@@ -1,6 +1,6 @@
<?php
/**
- * AdmissionPriority.class.php
+ * AdmissionPriority.php
*
* This class represents priorities a user has given to a set of courses.
* No instance is needed, all methods are designed to be called statically.
diff --git a/lib/classes/admission/AdmissionRule.class.php b/lib/classes/admission/AdmissionRule.php
index fe5e5bb..2b826e4 100644
--- a/lib/classes/admission/AdmissionRule.class.php
+++ b/lib/classes/admission/AdmissionRule.php
@@ -1,7 +1,7 @@
<?php
/**
- * AdmissionRule.class.php
+ * AdmissionRule.php
*
* An abstract representation of rules for course admission.
*
diff --git a/lib/classes/admission/AdmissionUserList.class.php b/lib/classes/admission/AdmissionUserList.php
index 570bc62..3e5a430 100644
--- a/lib/classes/admission/AdmissionUserList.class.php
+++ b/lib/classes/admission/AdmissionUserList.php
@@ -1,7 +1,7 @@
<?php
/**
- * AdmissionUserList.class.php
+ * AdmissionUserList.php
*
* Contains users that get different probabilities than others in seat
* distribution algorithm.
diff --git a/lib/classes/admission/CourseSet.class.php b/lib/classes/admission/CourseSet.php
index d94bc05..cf87667 100644
--- a/lib/classes/admission/CourseSet.class.php
+++ b/lib/classes/admission/CourseSet.php
@@ -1,7 +1,7 @@
<?php
/**
- * CourseSet.class.php
+ * CourseSet.php
*
* Represents groups of Stud.IP courses that have common rules for admission.
*
diff --git a/lib/classes/admission/RandomAlgorithm.class.php b/lib/classes/admission/RandomAlgorithm.php
index 54d44d7..4666f59 100644
--- a/lib/classes/admission/RandomAlgorithm.class.php
+++ b/lib/classes/admission/RandomAlgorithm.php
@@ -1,7 +1,7 @@
<?php
/**
- * RandomAlgorithm.class.php - Standard seat distribution algorithm
+ * RandomAlgorithm.php - Standard seat distribution algorithm
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
diff --git a/lib/classes/admission/UserFilter.class.php b/lib/classes/admission/UserFilter.php
index d380ef0..692422f 100644
--- a/lib/classes/admission/UserFilter.class.php
+++ b/lib/classes/admission/UserFilter.php
@@ -1,7 +1,7 @@
<?php
/**
- * UserFilter.class.php
+ * UserFilter.php
*
* Conditions for user selection in Stud.IP. A condition is a collection of
* condition fields, e.g. degree, course of study or semester. Each
diff --git a/lib/classes/admission/UserFilterField.class.php b/lib/classes/admission/UserFilterField.php
index 9081e90..4b51322 100644
--- a/lib/classes/admission/UserFilterField.class.php
+++ b/lib/classes/admission/UserFilterField.php
@@ -1,7 +1,7 @@
<?php
/**
- * UserFilterField.class.php
+ * UserFilterField.php
*
* A specification of a Stud.IP condition that must be fulfilled. One
* or more instances of the UserFilterField subclasses make up a
@@ -197,11 +197,10 @@ class UserFilterField
if (self::$available_filter_fields === null) {
$fields = [];
// Load all PHP class files found in the condition field folder.
- foreach (glob(realpath(dirname(__FILE__).'/userfilter').'/*.class.php') as $file) {
+ foreach (glob(realpath(dirname(__FILE__).'/userfilter').'/*.php') as $file) {
require_once($file);
// Try to auto-calculate class name from file name.
- $className = mb_substr(basename($file), 0,
- mb_strpos(basename($file), '.class.php'));
+ $className = mb_substr(basename($file), 0, mb_strpos(basename($file), '.php'));
// Check if class is right.
if (is_subclass_of($className, 'UserFilterField')) {
if ($className::$isParameterized) {
diff --git a/lib/classes/admission/userfilter/DatafieldCondition.class.php b/lib/classes/admission/userfilter/DatafieldCondition.php
index 966fe8e..1bc93e8 100644
--- a/lib/classes/admission/userfilter/DatafieldCondition.class.php
+++ b/lib/classes/admission/userfilter/DatafieldCondition.php
@@ -1,6 +1,6 @@
<?php
/**
- * DatafieldCondition.class.php
+ * DatafieldCondition.php
*
*
* This program is free software; you can redistribute it and/or
diff --git a/lib/classes/admission/userfilter/DegreeCondition.class.php b/lib/classes/admission/userfilter/DegreeCondition.php
index 9180e2d..61ce456 100644
--- a/lib/classes/admission/userfilter/DegreeCondition.class.php
+++ b/lib/classes/admission/userfilter/DegreeCondition.php
@@ -1,6 +1,6 @@
<?php
/**
- * DegreeCondition.class.php
+ * DegreeCondition.php
*
* All conditions concerning the study degree in Stud.IP can be specified here.
*
diff --git a/lib/classes/admission/userfilter/PermissionCondition.class.php b/lib/classes/admission/userfilter/PermissionCondition.php
index 4adfdbf..fe9458c 100644
--- a/lib/classes/admission/userfilter/PermissionCondition.class.php
+++ b/lib/classes/admission/userfilter/PermissionCondition.php
@@ -1,6 +1,6 @@
<?php
/**
- * PermissionCondition.class.php
+ * PermissionCondition.php
*
* All conditions concerning the semester of study in Stud.IP can be specified here.
*
diff --git a/lib/classes/admission/userfilter/SemesterOfStudyCondition.class.php b/lib/classes/admission/userfilter/SemesterOfStudyCondition.php
index 2c1233a..5794f75 100644
--- a/lib/classes/admission/userfilter/SemesterOfStudyCondition.class.php
+++ b/lib/classes/admission/userfilter/SemesterOfStudyCondition.php
@@ -1,6 +1,6 @@
<?php
/**
- * SemesterOfStudyCondition.class.php
+ * SemesterOfStudyCondition.php
*
* All conditions concerning the semester of study in Stud.IP can be specified here.
*
diff --git a/lib/classes/admission/userfilter/StgteilVersionCondition.class.php b/lib/classes/admission/userfilter/StgteilVersionCondition.php
index f6348e5..ec5c1f3 100644
--- a/lib/classes/admission/userfilter/StgteilVersionCondition.class.php
+++ b/lib/classes/admission/userfilter/StgteilVersionCondition.php
@@ -1,6 +1,6 @@
<?php
/**
- * StgteilVersionCondition.class.php
+ * StgteilVersionCondition.php
*
* All conditions concerning the Studiengangteil-Versionen in Stud.IP can be specified here.
*
diff --git a/lib/classes/admission/userfilter/SubjectCondition.class.php b/lib/classes/admission/userfilter/SubjectCondition.php
index ab44d49..7aa5f26 100644
--- a/lib/classes/admission/userfilter/SubjectCondition.class.php
+++ b/lib/classes/admission/userfilter/SubjectCondition.php
@@ -1,6 +1,6 @@
<?php
/**
- * SubjectCondition.class.php
+ * SubjectCondition.php
*
* All conditions concerning the study subject in Stud.IP can be specified here.
*
diff --git a/lib/classes/admission/userfilter/SubjectConditionAny.class.php b/lib/classes/admission/userfilter/SubjectConditionAny.php
index 107c86c..3a3712b 100644
--- a/lib/classes/admission/userfilter/SubjectConditionAny.class.php
+++ b/lib/classes/admission/userfilter/SubjectConditionAny.php
@@ -1,6 +1,6 @@
<?php
/**
- * SubjectConditionAny.class.php
+ * SubjectConditionAny.php
*
* All conditions concerning the study subject in Stud.IP can be specified here.
*
@@ -14,7 +14,7 @@
* @category Stud.IP
*/
-require_once realpath(__DIR__ . '/..') . '/UserFilterField.class.php';
+require_once realpath(__DIR__ . '/..') . '/UserFilterField.php';
class SubjectConditionAny extends UserFilterField
{
diff --git a/lib/classes/assets/SASSCompiler.php b/lib/classes/assets/SASSCompiler.php
index 2dcda2d..0b03a8c 100644
--- a/lib/classes/assets/SASSCompiler.php
+++ b/lib/classes/assets/SASSCompiler.php
@@ -2,7 +2,7 @@
namespace Assets;
use Assets;
-use StudipCacheFactory;
+use Studip\Cache\Factory;
use Studip;
use ScssPhp\ScssPhp\Compiler as ScssCompiler;
@@ -82,7 +82,7 @@ class SASSCompiler implements Compiler
*/
private function getPrefix()
{
- $cache = StudipCacheFactory::getCache();
+ $cache = Studip\Cache\Factory::getCache();
$prefix = $cache->read(self::CACHE_KEY);
diff --git a/lib/classes/auth_plugins/StudipAuthAbstract.class.php b/lib/classes/auth_plugins/StudipAuthAbstract.php
index 36c75df..19d5afa 100644
--- a/lib/classes/auth_plugins/StudipAuthAbstract.class.php
+++ b/lib/classes/auth_plugins/StudipAuthAbstract.php
@@ -1,7 +1,7 @@
<?php
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// StudipAuthAbstract.class.php
+// StudipAuthAbstract.php
// Abstract class, used as a template for authentication plugins
//
// Copyright (c) 2003 André Noack <noack@data-quest.de>
diff --git a/lib/classes/auth_plugins/StudipAuthCAS.class.php b/lib/classes/auth_plugins/StudipAuthCAS.php
index 29deb75..29deb75 100644
--- a/lib/classes/auth_plugins/StudipAuthCAS.class.php
+++ b/lib/classes/auth_plugins/StudipAuthCAS.php
diff --git a/lib/classes/auth_plugins/StudipAuthIP.class.php b/lib/classes/auth_plugins/StudipAuthIP.php
index e0d6afa..dd42a28 100644
--- a/lib/classes/auth_plugins/StudipAuthIP.class.php
+++ b/lib/classes/auth_plugins/StudipAuthIP.php
@@ -1,6 +1,6 @@
<?php
/*
- * StudipAuthIP.class.php - Stud.IP authentication with user ip
+ * StudipAuthIP.php - Stud.IP authentication with user ip
* Copyright (c) 2014 Florian Bieringer, Uni Passau
*
* This program is free software; you can redistribute it and/or
diff --git a/lib/classes/auth_plugins/StudipAuthLTI.class.php b/lib/classes/auth_plugins/StudipAuthLTI.php
index e8c316f..d5a2863 100644
--- a/lib/classes/auth_plugins/StudipAuthLTI.class.php
+++ b/lib/classes/auth_plugins/StudipAuthLTI.php
@@ -1,6 +1,6 @@
<?php
/*
- * StudipAuthLTI.class.php - Stud.IP authentication against LTI 1.1 consumer
+ * StudipAuthLTI.php - Stud.IP authentication against LTI 1.1 consumer
* Copyright (c) 2018 Elmar Ludwig
*
* This program is free software; you can redistribute it and/or
@@ -9,8 +9,12 @@
* the License, or (at your option) any later version.
*/
+use Studip\OAuth2\NegotiatesWithPsr7;
+
class StudipAuthLTI extends StudipAuthSSO
{
+ use NegotiatesWithPsr7;
+
public $consumer_keys;
public $username;
public $domain;
@@ -62,24 +66,15 @@ class StudipAuthLTI extends StudipAuthSSO
*
* @return bool true if authentication succeeds
*
- * @throws OAuthException2 if the signature verification failed
- *
*/
public function isAuthenticated($username, $password)
{
- require_once 'vendor/oauth-php/library/OAuthRequestVerifier.php';
-
- OAuthStore::instance('PDO', [
- 'dsn' => 'mysql:host=' . $GLOBALS['DB_STUDIP_HOST'] . ';dbname=' . $GLOBALS['DB_STUDIP_DATABASE'],
- 'username' => $GLOBALS['DB_STUDIP_USER'],
- 'password' => $GLOBALS['DB_STUDIP_PASSWORD']
- ]);
-
$consumer_key = Request::get('oauth_consumer_key');
$consumer_secret = $this->consumer_keys[$consumer_key]['consumer_secret'];
- $oarv = new OAuthRequestVerifier();
- $oarv->verifySignature($consumer_secret, false, false);
+ if (!Studip\OAuth1::verifyRequest($this->getPsrRequest(), $consumer_secret, '')) {
+ return false;
+ }
return parent::isAuthenticated($username, $password);
}
@@ -93,8 +88,6 @@ class StudipAuthLTI extends StudipAuthSSO
* @param string $password the password (ignored)
*
* @return mixed if authentication succeeds: the Stud.IP user, else false
- *
- * @throws OAuthException2 if the signature verification failed
*/
public function authenticateUser($username, $password)
{
diff --git a/lib/classes/auth_plugins/StudipAuthLdap.class.php b/lib/classes/auth_plugins/StudipAuthLdap.php
index 7cb8686..6bbd3fd 100644
--- a/lib/classes/auth_plugins/StudipAuthLdap.class.php
+++ b/lib/classes/auth_plugins/StudipAuthLdap.php
@@ -1,7 +1,7 @@
<?php
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// StudipAuthLdap.class.php
+// StudipAuthLdap.php
// Stud.IP authentication against LDAP Server
//
// Copyright (c) 2003 André Noack <noack@data-quest.de>
diff --git a/lib/classes/auth_plugins/StudipAuthLdapReadAndBind.class.php b/lib/classes/auth_plugins/StudipAuthLdapReadAndBind.php
index 742f0cb..3acb1d8 100644
--- a/lib/classes/auth_plugins/StudipAuthLdapReadAndBind.class.php
+++ b/lib/classes/auth_plugins/StudipAuthLdapReadAndBind.php
@@ -4,7 +4,7 @@
# Lifter010: TODO
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// StudipAuthLdapReadAndBind.class.php
+// StudipAuthLdapReadAndBind.php
// Stud.IP authentication against LDAP Server using read-only account and
// user bind
//
diff --git a/lib/classes/auth_plugins/StudipAuthOIDC.class.php b/lib/classes/auth_plugins/StudipAuthOIDC.php
index adfe9c9..b26c17b 100644
--- a/lib/classes/auth_plugins/StudipAuthOIDC.class.php
+++ b/lib/classes/auth_plugins/StudipAuthOIDC.php
@@ -1,6 +1,6 @@
<?php
/*
- * StudipAuthOpenID.class.php - Stud.IP authentication using OpenID Connect
+ * StudipAuthOpenID.php - Stud.IP authentication using OpenID Connect
* Copyright (c) 2021 André Noack <noack@data-quest.de>
*
* This program is free software; you can redistribute it and/or
diff --git a/lib/classes/auth_plugins/StudipAuthSSO.class.php b/lib/classes/auth_plugins/StudipAuthSSO.php
index 752fa59..dd6af11 100644
--- a/lib/classes/auth_plugins/StudipAuthSSO.class.php
+++ b/lib/classes/auth_plugins/StudipAuthSSO.php
@@ -3,7 +3,7 @@
# Lifter003: TODO
# Lifter010: TODO
/*
- * StudipAuthSSO.class.php - abstract base class for SSO auth plugins
+ * StudipAuthSSO.php - abstract base class for SSO auth plugins
* Copyright (c) 2007 Elmar Ludwig, Universitaet Osnabrueck
*
* This program is free software; you can redistribute it and/or
diff --git a/lib/classes/auth_plugins/StudipAuthShib.class.php b/lib/classes/auth_plugins/StudipAuthShib.php
index 3eedc65..135b3f6 100644
--- a/lib/classes/auth_plugins/StudipAuthShib.class.php
+++ b/lib/classes/auth_plugins/StudipAuthShib.php
@@ -3,7 +3,7 @@
# Lifter003: TODO
# Lifter010: TODO
/*
- * StudipAuthShib.class.php - Stud.IP authentication against Shibboleth server
+ * StudipAuthShib.php - Stud.IP authentication against Shibboleth server
* Copyright (c) 2007 Elmar Ludwig, Universitaet Osnabrueck
*
* This program is free software; you can redistribute it and/or
diff --git a/lib/classes/auth_plugins/StudipAuthStandard.class.php b/lib/classes/auth_plugins/StudipAuthStandard.php
index 5bb3e65..927a13c 100644
--- a/lib/classes/auth_plugins/StudipAuthStandard.class.php
+++ b/lib/classes/auth_plugins/StudipAuthStandard.php
@@ -4,7 +4,7 @@
# Lifter010: TODO
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
-// StudipAuthStandard.class.php
+// StudipAuthStandard.php
// Basic Stud.IP authentication, using the Stud.IP database
//
// Copyright (c) 2003 André Noack <noack@data-quest.de>
diff --git a/lib/classes/cache/Cache.php b/lib/classes/cache/Cache.php
new file mode 100644
index 0000000..be703cf
--- /dev/null
+++ b/lib/classes/cache/Cache.php
@@ -0,0 +1,213 @@
+<?php
+
+namespace Studip\Cache;
+
+use Psr\Cache\CacheItemInterface;
+use Psr\Cache\CacheItemPoolInterface;
+
+/**
+ * An abstract class which has to be extended by instances returned from
+ * \Studip\Cache\Factory#getCache
+ *
+ * @author Marco Diedrich (mdiedric@uos)
+ * @author Marcus Lunzenauer (mlunzena@uos.de)
+ * @author Moritz Strohm <strohm@data-quest.de>
+ * @copyright (c) Authors
+ * @since 1.6
+ * @license GPL2 or any later version
+ */
+abstract class Cache implements CacheItemPoolInterface
+{
+ const DEFAULT_EXPIRATION = 12 * 60 * 60; // 12 hours
+
+ /**
+ * @return string A translateable display name for this cache class.
+ */
+ abstract public static function getDisplayName(): string;
+
+ /**
+ * Get some statistics from cache, like number of entries, hit rate or
+ * whatever the underlying cache provides.
+ * Results are returned in form of an array like
+ * "[
+ * [
+ * 'name' => <displayable name>
+ * 'value' => <value of the current stat>
+ * ]
+ * ]"
+ *
+ * @return array
+ */
+ abstract public function getStats(): array;
+
+ /**
+ * Return the Vue component name and props that handle configuration.
+ * The associative array is of the form
+ * [
+ * 'component' => <Vue component name>,
+ * 'props' => <Properties for component>
+ * ]
+ *
+ * @return array
+ */
+ abstract public static function getConfig(): array;
+
+ /**
+ * Expire item from the cache.
+ *
+ * Example:
+ *
+ * # expires foo
+ * $cache->expire('foo');
+ *
+ * @param string $arg a single key
+ */
+ abstract public function expire($arg);
+
+ /**
+ * Expire all items from the cache.
+ */
+ abstract public function flush();
+
+ /**
+ * @see CacheItemPoolInterface::getItem
+ */
+ abstract public function getItem(string $key): CacheItemInterface;
+
+ /**
+ * @see CacheItemPoolInterface::hasItem
+ */
+ abstract public function hasItem(string $key): bool;
+
+ /**
+ * @var array An array of deferred items that shall be saved only
+ * when commit() is called. This is only used in PSR-6 cache methods.
+ */
+ protected array $deferred_items = [];
+
+ /**
+ * Retrieve item from the server.
+ *
+ * Example:
+ *
+ * # reads foo
+ * $foo = $cache->reads('foo');
+ *
+ * @param string $arg a single key
+ *
+ * @return mixed the previously stored data if an item with such a key
+ * exists on the server or FALSE on failure.
+ *
+ * @deprecated To be removed with Stud.IP 7.0.
+ */
+ public function read($arg)
+ {
+ $item = $this->getItem($arg);
+ if ($item->isHit()) {
+ return $item->get();
+ }
+ return false;
+ }
+
+ /**
+ * Store data at the server.
+ *
+ * @param string $name the item's key.
+ * @param mixed $content the item's content (will be serialized if necessary).
+ * @param int $expires the item's expiry time in seconds. Optional, defaults to 12h.
+ *
+ * @return bool returns TRUE on success or FALSE on failure.
+
+ * @deprecated To be removed with Stud.IP 7.0.
+ */
+ public function write($name, $content, $expires = self::DEFAULT_EXPIRATION)
+ {
+ $item = new Item($name, $content, $expires);
+
+ return $this->save($item);
+ }
+
+ /**
+ * Calculates the expiration by a cache item. If that cannot be determined,
+ * the default expiration period is returned.
+ *
+ * @param Item $item The item from which to get the expiration time.
+ *
+ * @return int The time from now until the expiration in seconds.
+ */
+ public function getExpiration(CacheItemInterface $item) : int
+ {
+ $expiration = self::DEFAULT_EXPIRATION;
+ if ($item instanceof Item) {
+ $expiration = $item->getExpirationInSeconds();
+ }
+ return $expiration;
+ }
+
+ // PSR-6 CacheItemPoolInterface:
+
+ /**
+ * @see CacheItemPoolInterface::getItems
+ */
+ public function getItems(array $keys = []): iterable
+ {
+ $items = [];
+ foreach ($keys as $key) {
+ $item = $this->getItem($key);
+ if ($item instanceof Item) {
+ $items[] = $item;
+ }
+ }
+ return $items;
+ }
+
+ /**
+ * @see CacheItemPoolInterface::clear
+ */
+ public function clear(): bool
+ {
+ $this->deferred_items = [];
+ $this->flush();
+ return true;
+ }
+
+ /**
+ * @see CacheItemPoolInterface::deleteItem
+ */
+ public function deleteItem($key): bool
+ {
+ $this->expire($key);
+ return true;
+ }
+
+ /**
+ * @see CacheItemPoolInterface::deleteItems
+ */
+ public function deleteItems(array $keys): bool
+ {
+ foreach ($keys as $key) {
+ $this->expire($key);
+ }
+ return true;
+ }
+
+ /**
+ * @see CacheItemPoolInterface::saveDeferred
+ */
+ public function saveDeferred(CacheItemInterface $item): bool
+ {
+ $this->deferred_items[] = $item;
+ return true;
+ }
+
+ /**
+ * @see CacheItemPoolInterface::commit
+ */
+ public function commit(): bool
+ {
+ foreach ($this->deferred_items as $item) {
+ $this->save($item);
+ }
+ return true;
+ }
+}
diff --git a/lib/classes/cache/DbCache.php b/lib/classes/cache/DbCache.php
new file mode 100644
index 0000000..c7abef2
--- /dev/null
+++ b/lib/classes/cache/DbCache.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace Studip\Cache;
+
+use DBManager;
+use Psr\Cache\CacheItemInterface;
+
+/**
+ * StudipCache implementation using database table
+ *
+ * @author Elmar Ludwig <elmar.ludwig@uos.de>
+ */
+class DbCache extends Cache
+{
+ /**
+ * @return string A display name (that can be translated) for this cache class.
+ */
+ public static function getDisplayName(): string
+ {
+ return _('Datenbank');
+ }
+
+ /**
+ * Expire item from the cache.
+ *
+ * @param string $arg a single key
+ */
+ public function expire($arg)
+ {
+ $db = DBManager::get();
+
+ $stmt = $db->prepare('DELETE FROM cache WHERE cache_key = ?');
+ $stmt->execute([$arg]);
+ }
+
+ /**
+ * Expire all items from the cache.
+ */
+ public function flush()
+ {
+ $db = DBManager::get();
+
+ $db->exec('TRUNCATE TABLE cache');
+ }
+
+ /**
+ * Delete all expired items from the cache.
+ */
+ public function purge()
+ {
+ $db = DBManager::get();
+
+ $stmt = $db->prepare('DELETE FROM cache WHERE expires < ?');
+ $stmt->execute([time()]);
+ }
+
+ /**
+ * Return statistics.
+ *
+ * @return array|array[]
+ *@see Cache::getStats()
+ *
+ */
+ public function getStats(): array
+ {
+ return [
+ __CLASS__ => [
+ 'name' => _('Anzahl Einträge'),
+ 'value' => DBManager::get()->fetchColumn("SELECT COUNT(*) FROM `cache`")
+ ]
+ ];
+ }
+
+ /**
+ * Return the Vue component name and props that handle configuration.
+ *
+ * @return array
+ *@see Cache::getConfig()
+ *
+ */
+ public static function getConfig(): array
+ {
+ return [
+ 'component' => null,
+ 'props' => []
+ ];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getItem(string $key): CacheItemInterface
+ {
+ $query = "SELECT `content`, `expires`
+ FROM `cache`
+ WHERE `cache_key` = :key
+ AND `expires` > UNIX_TIMESTAMP()";
+ $result = DBManager::get()->fetchOne($query, [':key' => $key]);
+
+ $item = new Item($key);
+ if (!empty($result)) {
+ $item->setHit();
+ if ($result['content']) {
+ $item->set(unserialize($result['content']));
+ }
+ if ($result['expires']) {
+ $expiration = new \DateTime();
+ $expiration->setTimestamp($result['expires']);
+ $item->expiresAt($expiration);
+ }
+ }
+ return $item;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function hasItem(string $key): bool
+ {
+ $query = "SELECT 1
+ FROM `cache`
+ WHERE `cache_key` = :key
+ AND `expires` > UNIX_TIMESTAMP()";
+ return (bool) DBManager::get()->fetchColumn($query, [':key' => $key]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save(CacheItemInterface $item): bool
+ {
+ $expiration = $this->getExpiration($item);
+ if ($expiration < 1) {
+ // The item would expire immediately.
+ return false;
+ }
+
+ return DBManager::get()->execute(
+ 'REPLACE INTO `cache` VALUES (?, ?, ?)',
+ [$item->getKey(), serialize($item->get()), $expiration]
+ );
+ }
+}
diff --git a/lib/classes/cache/Exception.php b/lib/classes/cache/Exception.php
new file mode 100644
index 0000000..061d090
--- /dev/null
+++ b/lib/classes/cache/Exception.php
@@ -0,0 +1,27 @@
+<?php
+/*
+ * CacheException.php
+ * This file is part of Stud.IP.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @author Moritz Strohm <strohm@data-quest.de>
+ * @copyright 2024
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ * @since 6.0
+ */
+
+namespace Studip\Cache;
+
+/**
+ * The CacheException class is an implementation of the CacheException interface
+ * of PSR-6 that behaves like a StudipException.
+ */
+class Exception extends \StudipException implements \Psr\Cache\CacheException
+{
+ //Nothing here, since there is nothing to implement.
+}
diff --git a/lib/classes/StudipCacheFactory.class.php b/lib/classes/cache/Factory.php
index 77c5973..b5c8359 100644
--- a/lib/classes/StudipCacheFactory.class.php
+++ b/lib/classes/cache/Factory.php
@@ -1,4 +1,15 @@
<?php
+
+namespace Studip\Cache;
+
+use Config;
+use DBSchemaVersion;
+use MessageBox;
+use PageLayout;
+use ReflectionClass;
+use StudipCacheOperation;
+use UnexpectedValueException;
+
/**
* This factory retrieves the instance of StudipCache configured for use in
* this Stud.IP installation.
@@ -12,30 +23,29 @@
* @since 1.6
* @license GPL2 or any later version
*/
-
-class StudipCacheFactory
+class Factory
{
/**
* the default cache class
*
* @var string
*/
- const DEFAULT_CACHE_CLASS = StudipDbCache::class;
+ const DEFAULT_CACHE_CLASS = DbCache::class;
/**
* singleton instance
*
- * @var StudipCache
+ * @var Cache|null
*/
- private static $cache;
+ private static ?Cache $cache = null;
/**
* config instance
*
- * @var Config
+ * @var Config|null
*/
- private static $config = null;
+ private static ?Config $config = null;
/**
@@ -49,7 +59,7 @@ class StudipCacheFactory
*/
public static function getConfig()
{
- return is_null(self::$config) ? Config::getInstance() : self::$config;
+ return self::$config ?? Config::getInstance();
}
@@ -58,7 +68,7 @@ class StudipCacheFactory
* determine the class of the implementation of interface
* StudipCache
*/
- public static function setConfig($config)
+ public static function setConfig(Config $config)
{
self::$config = $config;
self::$cache = NULL;
@@ -77,15 +87,15 @@ class StudipCacheFactory
*
* @param bool $apply_proxied_operations Whether or not to apply any
* proxied (disable this in tests!)
- * @return StudipCache the cache instance
+ * @return Cache the cache instance
*/
- public static function getCache($apply_proxied_operations = true)
+ public static function getCache(bool $apply_proxied_operations = true): Cache
{
- if (is_null(self::$cache)) {
+ if (self::$cache === null) {
$proxied = false;
if (!$GLOBALS['CACHING_ENABLE']) {
- self::$cache = new StudipMemoryCache();
+ self::$cache = new MemoryCache();
// Proxy cache operations if CACHING_ENABLE is different from the globally set
// caching value. This should only be the case in cli mode.
@@ -98,7 +108,7 @@ class StudipCacheFactory
$args = self::retrieveConstructorArguments();
self::$cache = self::instantiateCache($class, $args);
- } catch (Exception $e) {
+ } catch (\Exception $e) {
error_log(__METHOD__ . ': ' . $e->getMessage());
PageLayout::addBodyElements(MessageBox::error(__METHOD__ . ': ' . $e->getMessage()));
$class = self::DEFAULT_CACHE_CLASS;
@@ -109,7 +119,7 @@ class StudipCacheFactory
// If proxy should be used, inject it. Otherwise apply pending
// operations, if any.
if ($proxied) {
- self::$cache = new StudipCacheProxy(self::$cache);
+ self::$cache = new Proxy(self::$cache);
} elseif ($GLOBALS['CACHING_ENABLE'] && $apply_proxied_operations) {
// Even if the above condition will try to eliminate most
// failures, the following operation still needs to be wrapped
@@ -118,7 +128,7 @@ class StudipCacheFactory
// for said operation.
try {
StudipCacheOperation::apply(self::$cache);
- } catch (Exception $e) {
+ } catch (\Exception $e) {
}
}
}
@@ -174,10 +184,11 @@ class StudipCacheFactory
* memory cache is instantiated, the cache will be wrapped in a wrapper
* class that uses a memory cache to reduce accesses to the cache.
*
- * @param string $class the name of the class
- * @param array $arguments an array of arguments to be used by the constructor
+ * @param string $class the name of the class
+ * @param array $arguments an array of arguments to be used by the constructor
*
- * @return StudipCache an instance of the specified class
+ * @return Cache an instance of the specified class
+ * @throws \ReflectionException
*/
public static function instantiateCache($class, $arguments)
{
@@ -186,8 +197,8 @@ class StudipCacheFactory
? $reflection_class->newInstanceArgs($arguments['config'])
: $reflection_class->newInstance();
- if ($class !== StudipMemoryCache::class) {
- return new StudipCacheWrapper($cache);
+ if ($class !== MemoryCache::class) {
+ return new Wrapper($cache);
}
return $cache;
diff --git a/lib/classes/StudipFileCache.class.php b/lib/classes/cache/FileCache.php
index 9eae66c..d760f08 100644
--- a/lib/classes/StudipFileCache.class.php
+++ b/lib/classes/cache/FileCache.php
@@ -1,46 +1,28 @@
<?php
-# Lifter010: TODO
-// +--------------------------------------------------------------------------+
-// This file is part of Stud.IP
-// StudipFileCache.class.php
-//
-//
-//
-// Copyright (c) 2007 André Noack <noack@data-quest.de>
-// +--------------------------------------------------------------------------+
-// This program is free software; you can redistribute it and/or
-// modify it under the terms of the GNU General Public License
-// as published by the Free Software Foundation; either version 2
-// of the License, or any later version.
-// +--------------------------------------------------------------------------+
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-// +--------------------------------------------------------------------------+
+
+namespace Studip\Cache;
+
+use Config;
+use Exception;
+use Psr\Cache\CacheItemInterface;
/**
- * StudipCache implementation using files
- *
- * @package studip
- * @subpackage cache
+ * Cache implementation using files
*
* @author André Noack <noack@data-quest.de>
- * @version 2
+ * @copyright 2007 André Noack <noack@data-quest.de>
+ * @license GPL2 or any later version
*/
-class StudipFileCache implements StudipCache
+class FileCache extends Cache
{
- use StudipCacheKeyTrait;
+ use KeyTrait;
/**
* full path to cache directory
*
* @var string
*/
- private $dir;
+ private string $dir;
/**
* @return string A translateable display name for this cache class.
@@ -55,26 +37,28 @@ class StudipFileCache implements StudipCache
* $CACHING_FILECACHE_PATH or is set to
* $TMP_PATH/studip_cache
*
- * @param string the path to use
- * @return void
- * @throws exception if the directory does not exist or could not be
+ * @param string $path the path to use
+ * @throws Exception if the directory does not exist or could not be
* created
*/
- public function __construct($path = '')
+ public function __construct(string $path = '')
{
$this->dir = $path
- ?: (Config::get()->SYSTEMCACHE['type'] == 'StudipFileCache' ?
- Config::get()->SYSTEMCACHE['config']['path'] : '')
+ ?: (
+ Config::get()->SYSTEMCACHE['type'] === self::class
+ ? Config::get()->SYSTEMCACHE['config']['path']
+ : ''
+ )
?: $GLOBALS['CACHING_FILECACHE_PATH']
?: ($GLOBALS['TMP_PATH'] . '/' . 'studip_cache');
$this->dir = rtrim($this->dir, '\\/') . '/';
if (!is_dir($this->dir) && !@mkdir($this->dir, 0700)) {
- throw new Exception('Could not create directory: ' . $this->dir);
+ throw new \Exception('Could not create directory: ' . $this->dir);
}
if (!is_writable($this->dir)) {
- throw new Exception('Can not write to directory: ' . $this->dir);
+ throw new \Exception('Can not write to directory: ' . $this->dir);
}
}
@@ -91,9 +75,11 @@ class StudipFileCache implements StudipCache
/**
* expire cache item
*
- * @see StudipCache::expire()
* @param string $arg
+ *
* @return void
+ * @throws Exception
+ * @see Cache::expire()
*/
public function expire($arg)
{
@@ -113,61 +99,20 @@ class StudipFileCache implements StudipCache
}
/**
- * retrieve cache item from filesystem
- * tests first if item is expired
- *
- * @see StudipCache::read()
- * @param string $arg a cache key
- * @return string|bool
- */
- public function read($arg)
- {
- $key = $this->getCacheKey($arg);
-
- if ($file = $this->check($key)){
- $f = @fopen($file, 'rb');
- if ($f) {
- @flock($f, LOCK_SH);
- $result = stream_get_contents($f);
- @fclose($f);
- }
- return unserialize($result);
- }
- return false;
- }
-
- /**
- * store data as cache item in filesystem
- *
- * @see StudipCache::write()
- * @param string $arg a cache key
- * @param mixed $content data to store
- * @param int $expire expiry time in seconds, default 12h
- * @return int|bool the number of bytes that were written to the file,
- * or false on failure
- */
- public function write($arg, $content, $expire = self::DEFAULT_EXPIRATION)
- {
- $key = $this->getCacheKey($arg);
-
- $this->expire($key);
- $file = $this->getPathAndFile($key, $expire);
- return @file_put_contents($file, serialize($content), LOCK_EX);
- }
-
- /**
* checks if specified cache item is expired
* if expired the cache file is deleted
*
* @param string $key a cache key to check
- * @return string|bool the path to the cache file or false if expired
+ *
+ * @return array|bool the path to the cache file or false if expired
+ * @throws Exception
*/
private function check($key)
{
if ($file = $this->getPathAndFile($key)){
- list($id, $expire) = explode('-', basename($file));
+ [$id, $expire] = explode('-', basename($file));
if (time() < $expire) {
- return $file;
+ return [$file, $expire];
} else {
@unlink($file);
}
@@ -183,16 +128,18 @@ class StudipFileCache implements StudipCache
* the filename is constructed from the hashed cache key
* and the timestamp of expiration
*
- * @param string $key a cache key
- * @param int $expire expiry time in seconds
+ * @param string $key a cache key
+ * @param int|null $expire expiry time in seconds
+ *
* @return string|bool full path to cache item or false on failure
+ * @throws Exception
*/
- private function getPathAndFile($key, $expire = null)
+ private function getPathAndFile(string $key, ?int $expire = null): bool|string
{
$id = hash('md5', $key);
$path = $this->dir . mb_substr($id, 0, 2);
if (!is_dir($path) && !@mkdir($path, 0700)) {
- throw new Exception('Could not create directory: ' . $path);
+ throw new \Exception('Could not create directory: ' . $path);
}
if (!is_null($expire)){
return $path . '/' . $id . '-' . (time() + $expire);
@@ -208,16 +155,17 @@ class StudipFileCache implements StudipCache
/**
* purges expired entries from the cache directory
*
- * @param bool echo messages if set to false
+ * @param bool $be_quiet echo messages if set to false
+ *
* @return int the number of deleted files
*/
- public function purge($be_quiet = true)
+ public function purge(bool $be_quiet = true): int
{
$now = time();
$deleted = 0;
foreach (@glob($this->dir . '*', GLOB_ONLYDIR) as $current_dir){
foreach (@glob("{$current_dir}/*") as $file){
- list($id, $expire) = explode('-', basename($file));
+ [$id, $expire] = explode('-', basename($file));
if ($expire < $now) {
if (@unlink($file)) {
++$deleted;
@@ -225,8 +173,8 @@ class StudipFileCache implements StudipCache
echo "File: {$file} deleted.\n";
}
}
- } else if (!$be_quiet){
- echo "File: {$file} expires on " . strftime('%x %X', $expire) . "\n";
+ } else if (!$be_quiet) {
+ echo "File: {$file} expires on " . date('Y-m-d H:i:s', $expire) . "\n";
}
}
}
@@ -243,7 +191,7 @@ class StudipFileCache implements StudipCache
return [
__CLASS__ => [
'name' => _('Anzahl Einträge'),
- 'value' => DBManager::get()->fetchColumn("SELECT COUNT(*) FROM `cache`")
+ 'value' => \DBManager::get()->fetchColumn("SELECT COUNT(*) FROM `cache`")
]
];
}
@@ -273,4 +221,58 @@ class StudipFileCache implements StudipCache
];
}
+ /**
+ * @inheritDoc
+ */
+ public function getItem(string $key): CacheItemInterface
+ {
+ $real_key = $this->getCacheKey($key);
+
+ $item = new \Studip\Cache\Item($key);
+
+ $file_data = $this->check($real_key);
+ if ($file_data) {
+ $file = $file_data[0];
+ $expire = $file_data[1];
+ $f = @fopen($file, 'rb');
+ if ($f) {
+ @flock($f, LOCK_SH);
+ $result = stream_get_contents($f);
+ @fclose($f);
+ }
+ $item->setHit();
+ $item->set(unserialize($result));
+ $expiration = new \DateTime();
+ $expiration->setTimestamp($expire);
+ $item->expiresAt($expiration);
+ }
+ return $item;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function hasItem(string $key): bool
+ {
+ $real_key = $this->getCacheKey($key);
+ $file_data = $this->check($real_key);
+ return $file_data !== false;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save(CacheItemInterface $item): bool
+ {
+ $expiration = $this->getExpiration($item);
+ if ($expiration < 1) {
+ //The item would expire immediately.
+ return false;
+ }
+
+ $real_key = $this->getCacheKey($item->getKey());
+ $this->expire($real_key);
+ $file = $this->getPathAndFile($real_key, $expiration);
+ return @file_put_contents($file, serialize($item->get()), LOCK_EX);
+ }
}
diff --git a/lib/classes/cache/InvalidCacheArgumentException.php b/lib/classes/cache/InvalidCacheArgumentException.php
new file mode 100644
index 0000000..a201cad
--- /dev/null
+++ b/lib/classes/cache/InvalidCacheArgumentException.php
@@ -0,0 +1,28 @@
+<?php
+/*
+ * CacheException.php
+ * This file is part of Stud.IP.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @author Moritz Strohm <strohm@data-quest.de>
+ * @copyright 2024
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ * @since 6.0
+ */
+
+namespace Studip\Cache;
+
+
+/**
+ * The InvalidCacheArgumentException is an implementation of the InvalidArgumentException interface
+ * of PSR-6 that behaves like a StudipException.
+ */
+class InvalidCacheArgumentException extends \StudipException implements \Psr\Cache\InvalidArgumentException
+{
+ //Nothing here, since there is nothing to implement.
+}
diff --git a/lib/classes/cache/Item.php b/lib/classes/cache/Item.php
new file mode 100644
index 0000000..99a6df8
--- /dev/null
+++ b/lib/classes/cache/Item.php
@@ -0,0 +1,164 @@
+<?php
+/**
+ * Item.php
+ * This file is part of Stud.IP.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @author Moritz Strohm <strohm@data-quest.de>
+ * @copyright 2024
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ * @since 6.0
+ */
+
+namespace Studip\Cache;
+
+use DateInterval;
+use DateTime;
+use Psr\Cache\CacheItemInterface;
+
+/**
+ * \Studip\Cache\CacheItem implements the CacheItemInterface of PSR-6. It holds the value and the
+ * key of a cache item and also provides additional methods to get the expiration of the item.
+ */
+class Item implements CacheItemInterface
+{
+ /**
+ * @var string The key of the item in the cache.
+ */
+ protected string $key;
+
+ /**
+ * @var mixed The value of the item.
+ */
+ protected mixed $value;
+
+ /**
+ * @var DateTime|null The expiration as DateTime object or null if the expiration is not defined.
+ */
+ protected ?DateTime $expiration = null;
+
+ /**
+ * @var bool An indicator whether the item has been found in the cache (true) or not (false).
+ */
+ protected bool $cache_hit = false;
+
+ /**
+ * The constructor of \Studip\Cache\CacheItem.
+ *
+ * @param string $key The key of the item in the cache.
+ * @param mixed $value The value of the item.
+ * @param int|null $expiration The expiration of the item in seconds, if applicable.
+ * @param bool $cache_hit Whether the item shall be constructed as cache hit (true) or not (false).
+ *
+ */
+ public function __construct(
+ string $key,
+ mixed $value = null,
+ ?int $expiration = null,
+ bool $cache_hit = false
+ ) {
+ $this->key = $key;
+ $this->value = $value;
+ $this->cache_hit = $cache_hit;
+ $this->expiresAfter($expiration);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getKey(): string
+ {
+ return $this->key;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function get(): mixed
+ {
+ return $this->value;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isHit(): bool
+ {
+ return $this->cache_hit;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function set($value): static
+ {
+ $this->value = $value;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function expiresAt($expiration): static
+ {
+ $this->expiration = $expiration;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function expiresAfter($time): static
+ {
+ $this->expiration = new DateTime();
+ if ($time instanceof DateInterval) {
+ $this->expiration = $this->expiration->add($time);
+ } elseif (is_integer($time)) {
+ $this->expiration->setTimestamp(time() + $time);
+ } else {
+ $this->expiration->setTimestamp(time() + Cache::DEFAULT_EXPIRATION);
+ }
+ return $this;
+ }
+
+ // \Studip\Cache\CacheItem specific methods:
+
+ /**
+ * Sets the item to be a cache hit.
+ *
+ * @return void
+ */
+ public function setHit() : void
+ {
+ $this->cache_hit = true;
+ }
+
+ /**
+ * Returns the expiration, if set.
+ *
+ * @return DateTime|null A DateTime object with the expiration date and time
+ * or null if the expiration is not defined.
+ */
+ public function getExpiration() : ?DateTime
+ {
+ return $this->expiration;
+ }
+
+ /**
+ * Returns the seconds from the current timestamp until the expiration of the item.
+ *
+ * @return int The seconds until the item expires
+ */
+ public function getExpirationInSeconds() : int
+ {
+ if ($this->expiration) {
+ return $this->expiration->getTimestamp() - time();
+ }
+ return 0;
+ }
+}
diff --git a/lib/classes/StudipCacheKeyTrait.php b/lib/classes/cache/KeyTrait.php
index 62eb142..021aaba 100644
--- a/lib/classes/StudipCacheKeyTrait.php
+++ b/lib/classes/cache/KeyTrait.php
@@ -1,4 +1,6 @@
<?php
+namespace Studip\Cache;
+
/**
* Trait for unique cache hashes per key for each system based on db configuration which should
* be sufficient to eliminate cache mishaps.
@@ -9,9 +11,9 @@
* @subpackage cache
* @since Stud.IP 5.0
*/
-trait StudipCacheKeyTrait
+trait KeyTrait
{
- protected $cache_prefix = null;
+ protected ?string $cache_prefix = null;
/**
* Returns a prefix cache key based on db configuration.
@@ -19,11 +21,11 @@ trait StudipCacheKeyTrait
* @param string $offset
* @return string
*/
- protected function getCacheKey($offset)
+ protected function getCacheKey(string $offset): string
{
if ($this->cache_prefix === null) {
$this->cache_prefix = md5("{$GLOBALS['DB_STUDIP_HOST']}|{$GLOBALS['DB_STUDIP_DATABASE']}");
}
- return "{$this->cache_prefix}/{$offset}";
+ return "$this->cache_prefix/$offset";
}
}
diff --git a/lib/classes/StudipMemcachedCache.php b/lib/classes/cache/MemcachedCache.php
index 0e44dd5..1c4b685 100644
--- a/lib/classes/StudipMemcachedCache.php
+++ b/lib/classes/cache/MemcachedCache.php
@@ -1,31 +1,23 @@
<?php
-/**
- * Copyright (C) 2007 - Marcus Lunzenauer <mlunzena@uos.de>
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- */
+namespace Studip\Cache;
+use Memcached;
+use Psr\Cache\CacheItemInterface;
/**
* Cache implementation using memcached.
*
- * @package studip
- * @subpackage cache
- *
- * @author mlunzena
+ * @author Marcus Lunzenauer <mlunzena@uos.de>
* @copyright (c) Authors
+ * @license GPL2 or any later version
* @since 5.0
*/
-
-class StudipMemcachedCache implements StudipCache
+class MemcachedCache extends Cache
{
- use StudipCacheKeyTrait;
+ use KeyTrait;
- private $memcache;
+ private Memcached $memcache;
/**
* @return string A translateable display name for this cache class.
@@ -38,18 +30,18 @@ class StudipMemcachedCache implements StudipCache
public function __construct($servers)
{
if (!extension_loaded('memcached')) {
- throw new Exception('Memcache extension missing.');
+ throw new \Exception('Memcache extension missing.');
}
- $prefix = Config::get()->STUDIP_INSTALLATION_ID;
- $this->memcache = new Memcached('studip' . $prefix ? '-' . $prefix : '');
+ $prefix = \Config::get()->STUDIP_INSTALLATION_ID;
+ $this->memcache = new Memcached('studip' . ($prefix ? '-' . $prefix : ''));
if (count($this->memcache->getServerList()) === 0) {
foreach ($servers as $server) {
$status = $this->memcache->addServer($server['hostname'], (int) $server['port']);
if (!$status) {
- throw new Exception("Could not add server: {$server['hostname']} @ port {$server['port']}");
+ throw new \Exception("Could not add server: {$server['hostname']} @ port {$server['port']}");
}
}
}
@@ -81,40 +73,6 @@ class StudipMemcachedCache implements StudipCache
}
/**
- * Retrieve item from the server.
- *
- * Example:
- *
- * # reads foo
- * $foo = $cache->reads('foo');
- *
- * @param string $arg a single key
- * @returns mixed the previously stored data if an item with such a key
- * exists on the server or FALSE on failure.
- */
- public function read($arg)
- {
- $key = $this->getCacheKey($arg);
- return $this->memcache->get($key);
- }
-
- /**
- * Store data at the server.
- *
- * @param string $arg the item's key.
- * @param string $content the item's content.
- * @param int $expire the item's expiry time in seconds. Defaults to 12h.
- *
- * @returns mixed returns TRUE on success or FALSE on failure.
- *
- */
- public function write($arg, $content, $expire = self::DEFAULT_EXPIRATION)
- {
- $key = $this->getCacheKey($arg);
- return $this->memcache->set($key, $content, $expire);
- }
-
- /**
* Return statistics.
*
* @StudipCache::getStats()
@@ -123,20 +81,19 @@ class StudipMemcachedCache implements StudipCache
*/
public function getStats(): array
{
- $stats = $this->memcache->getStats();
- return $stats;
+ return $this->memcache->getStats();
}
/**
* Return the Vue component name and props that handle configuration.
*
- * @see StudipCache::getConfig()
+ * @see Cache::getConfig()
*
* @return array
*/
public static function getConfig(): array
{
- $currentCache = Config::get()->SYSTEMCACHE;
+ $currentCache = \Config::get()->SYSTEMCACHE;
// Set default config for this cache
$currentConfig = [
@@ -154,4 +111,41 @@ class StudipMemcachedCache implements StudipCache
];
}
+ /**
+ * @inheritDoc
+ */
+ public function getItem(string $key): CacheItemInterface
+ {
+ $item = new Item($key);
+ $value = $this->memcache->get($this->getCacheKey($key));
+ if ($this->memcache->getResultCode() !== Memcached::RES_NOTFOUND) {
+ // Set the value, even if it is the boolean value false:
+ $item->setHit();
+ $item->set($value);
+ }
+ return $item;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function hasItem(string $key): bool
+ {
+ return $this->memcache->checkKey($this->getCacheKey($key));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save(CacheItemInterface $item): bool
+ {
+ $expiration = $this->getExpiration($item);
+ if ($expiration < 1) {
+ // The item would expire immediately.
+ return false;
+ }
+
+ $real_key = $this->getCacheKey($item->getKey());
+ return $this->memcache->set($real_key, $item->get(), $expiration);
+ }
}
diff --git a/lib/classes/cache/MemoryCache.php b/lib/classes/cache/MemoryCache.php
new file mode 100644
index 0000000..7c00753
--- /dev/null
+++ b/lib/classes/cache/MemoryCache.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Studip\Cache;
+
+use DateTime;
+use Psr\Cache\CacheItemInterface;
+
+/**
+ * The php memory implementation of the StudipCache interface.
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @license GPL2 or any later version
+ * @since Stud.IP 5.0
+ */
+class MemoryCache extends Cache
+{
+ protected array $memory_cache = [];
+
+ /**
+ * Expires just a single key.
+ *
+ * @param string $arg the key
+ */
+ public function expire($arg)
+ {
+ unset($this->memory_cache[$arg]);
+ }
+
+ /**
+ * Expire all items from the cache.
+ */
+ public function flush()
+ {
+ $this->memory_cache = [];
+ }
+
+ public static function getDisplayName(): string
+ {
+ return 'Memory cache';
+ }
+
+ public function getStats(): array
+ {
+ return [];
+ }
+
+ public static function getConfig(): array
+ {
+ return [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getItem(string $key): CacheItemInterface
+ {
+ $item = new Item($key);
+ if (!isset($this->memory_cache[$key])) {
+ return $item;
+ }
+ if ($this->memory_cache[$key]['expires'] < time()) {
+ $this->expire($key);
+ return $item;
+ }
+ $item->setHit();
+ $item->set($this->memory_cache[$key]['data']);
+ if (!empty($this->memory_cache[$key]['expires'])) {
+ $expiration = new DateTime();
+ $expiration->setTimestamp($this->memory_cache[$key]['expires']);
+ $item->expiresAt($expiration);
+ }
+ return $item;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function hasItem(string $key): bool
+ {
+ return isset($this->memory_cache[$key])
+ && $this->memory_cache[$key]['expires'] < time();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save(CacheItemInterface $item): bool
+ {
+ $expiration = $this->getExpiration($item);
+
+ $this->memory_cache[$item->getKey()] = [
+ 'expires' => $expiration + time(),
+ 'data' => $item->get(),
+ ];
+
+ return true;
+ }
+}
diff --git a/lib/classes/StudipCacheProxy.php b/lib/classes/cache/Proxy.php
index 686f812..fef034d 100644
--- a/lib/classes/StudipCacheProxy.php
+++ b/lib/classes/cache/Proxy.php
@@ -1,4 +1,10 @@
<?php
+
+namespace Studip\Cache;
+
+use Psr\Cache\CacheItemInterface;
+use StudipCacheOperation;
+
/**
* Proxies a StudipCache and stores the expire operation in the database.
* These operations are lateron applied to the cache they should have
@@ -8,18 +14,18 @@
* @license GPL2 or any later version
* @since Stud.IP 3.3
*/
-class StudipCacheProxy implements StudipCache
+class Proxy extends Cache
{
- protected $actual_cache;
- protected $proxy_these;
+ protected Cache $actual_cache;
+ protected array $proxy_these;
/**
- * @param StudipCache $cache The actual cache object
- * @param mixed $proxy_these List of operations to proxy (should be
- * an array but a space seperated string
- * is also valid)
+ * @param Cache $cache The actual cache object
+ * @param mixed $proxy_these List of operations to proxy (should be an
+ * array but a space seperated string is also
+ * valid)
*/
- public function __construct(StudipCache $cache, $proxy_these = ['expire'])
+ public function __construct(Cache $cache, $proxy_these = ['expire'])
{
if (!is_array($proxy_these)) {
$proxy_these = words($proxy_these);
@@ -34,20 +40,20 @@ class StudipCacheProxy implements StudipCache
/**
* Expires just a single key.
*
- * @param string $key The item's key
+ * @param string $arg The item's key
*/
- public function expire($key)
+ public function expire($arg)
{
if (in_array('expire', $this->proxy_these)) {
try {
- $operation = new StudipCacheOperation([$key, 'expire']);
+ $operation = new StudipCacheOperation([$arg, 'expire']);
$operation->parameters = serialize([]);
$operation->store();
- } catch (Exception $e) {
+ } catch (\Exception) {
}
}
- return $this->actual_cache->expire($key);
+ return $this->actual_cache->expire($arg);
}
/**
@@ -60,58 +66,58 @@ class StudipCacheProxy implements StudipCache
$operation = new StudipCacheOperation(['', 'flush']);
$operation->parameters = serialize([]);
$operation->store();
- } catch (Exception $e) {
+ } catch (\Exception) {
}
}
return $this->actual_cache->flush();
}
- /**
- * Reads just a single key from the cache.
- *
- * @param string $key The item's key
- * @return mixed The corresponding value
- */
- public function read($key)
+ public static function getDisplayName(): string
{
- return $this->actual_cache->read($key);
+ return static::class;
}
- /**
- * Store data at the server.
- *
- * @param string $key The item's key
- * @param string $content The item's conten
- * @param int $expires The item's expiry time in seconds, defaults to 12h
- * @return bool Returns TRUE on success or FALSE on failure
- */
- public function write($key, $content, $expires = self::DEFAULT_EXPIRATION)
+ public function getStats(): array
{
- if (in_array('write', $this->proxy_these)) {
- try {
- $operation = new StudipCacheOperation([$key, 'write']);
- $operation->parameters = serialize([$content, $expires]);
- $operation->store();
- } catch (Exception $e) {
- }
- }
+ return $this->actual_cache->getStats();
+ }
- return $this->actual_cache->write($key, $content, $expires);
+ public static function getConfig(): array
+ {
+ return [];
}
- public static function getDisplayName(): string
+ /**
+ * @inheritDoc
+ */
+ public function getItem(string $key): CacheItemInterface
{
- return static::class;
+ return $this->actual_cache->getItem($key);
}
- public function getStats(): array
+ /**
+ * @inheritDoc
+ */
+ public function hasItem(string $key): bool
{
- return $this->actual_cache->getStats();
+ return $this->actual_cache->hasItem($key);
}
- public static function getConfig(): array
+ /**
+ * @inheritDoc
+ */
+ public function save(CacheItemInterface $item): bool
{
- return [];
+ if (in_array('save', $this->proxy_these)) {
+ try {
+ $operation = new StudipCacheOperation([$item->getKey(), 'save']);
+ $operation->parameters = serialize([$item]);
+ $operation->store();
+ } catch (\Exception) {
+ }
+ }
+
+ return $this->actual_cache->save($item);
}
}
diff --git a/lib/classes/StudipRedisCache.class.php b/lib/classes/cache/RedisCache.php
index 7b9570b..a78f751 100644
--- a/lib/classes/StudipRedisCache.class.php
+++ b/lib/classes/cache/RedisCache.php
@@ -1,4 +1,15 @@
<?php
+
+namespace Studip\Cache;
+
+use BadMethodCallException;
+use Config;
+use DateTime;
+use Exception;
+use Psr\Cache\CacheItemInterface;
+use Redis;
+use RedisException;
+
/**
* Cache implementation using redis.
*
@@ -8,9 +19,9 @@
* @subpackage cache
* @since Stud.IP 5.0
*/
-class StudipRedisCache implements StudipCache
+class RedisCache extends Cache
{
- use StudipCacheKeyTrait;
+ use KeyTrait;
private $redis;
@@ -28,6 +39,8 @@ class StudipRedisCache implements StudipCache
* @param string $hostname Hostname of redis server
* @param int $port Port of redis server
* @param string $auth Optional auth token/password
+ *
+ * @throws RedisException
*/
public function __construct($hostname, $port, string $auth = '')
{
@@ -74,41 +87,6 @@ class StudipRedisCache implements StudipCache
}
/**
- * Retrieve item from the server.
- *
- * Example:
- *
- * # reads foo
- * $foo = $cache->reads('foo');
- *
- * @param string $arg a single key
- * @return mixed the previously stored data if an item with such a key
- * exists on the server or FALSE on failure.
- */
- public function read($arg)
- {
- $key = $this->getCacheKey($arg);
-
- $result = $this->redis->get($key);
-
- return ($result === null) ? null : unserialize($result);
- }
-
- /**
- * Store data at the server.
- *
- * @param string the item's key.
- * @param string the item's content.
- * @param int the item's expiry time in seconds. Defaults to 12h.
- * @return mixed returns TRUE on success or FALSE on failure.
- */
- public function write($name, $content, $expire = self::DEFAULT_EXPIRATION)
- {
- $key = $this->getCacheKey($name);
- return $this->redis->setEx($key, $expire, serialize($content));
- }
-
- /**
* Expire all items from the cache.
*/
public function flush()
@@ -174,4 +152,47 @@ class StudipRedisCache implements StudipCache
'props' => $currentConfig
];
}
+
+ /**
+ * @inheritDoc
+ */
+ public function getItem(string $key): CacheItemInterface
+ {
+ $item = new Item($key);
+ $real_key = $this->getCacheKey($key);
+ $result = $this->redis->get($real_key);
+ if ($result === null) {
+ return $item;
+ }
+ $item->setHit();
+ $item->set(unserialize($result));
+ $expiration = new DateTime();
+ $expiration->setTimestamp($this->redis->expiretime($real_key));
+ $item->expiresAt($expiration);
+ return $item;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function hasItem(string $key): bool
+ {
+ $real_key = $this->getCacheKey($key);
+ return $this->redis->get($real_key) !== null;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save(CacheItemInterface $item): bool
+ {
+ $expiration = $this->getExpiration($item);
+ if ($expiration < 1) {
+ // The item would expire immediately.
+ return false;
+ }
+
+ $real_key = $this->getCacheKey($item->getKey());
+ return $this->redis->setEx($real_key, $expiration, serialize($item->get()));
+ }
}
diff --git a/lib/classes/StudipCacheWrapper.php b/lib/classes/cache/Wrapper.php
index 6c75c01..4e6342c 100644
--- a/lib/classes/StudipCacheWrapper.php
+++ b/lib/classes/cache/Wrapper.php
@@ -1,5 +1,9 @@
<?php
+namespace Studip\Cache;
+
+use Psr\Cache\CacheItemInterface;
+
/**
* The cache wrapper wraps a memory cache around another cache. This should
* reduce the accesses to the actual cache.
@@ -8,17 +12,15 @@
* @license GPL2 or any later version
* @since Stud.IP 5.4
*/
-class StudipCacheWrapper implements StudipCache
+class Wrapper extends Cache
{
- const DEFAULT_MEMORY_EXPIRATION = 60;
-
- protected $actual_cache;
- protected $memory_cache;
+ protected Cache $actual_cache;
+ protected MemoryCache $memory_cache;
- public function __construct(StudipCache $actual_cache)
+ public function __construct(Cache $actual_cache)
{
$this->actual_cache = $actual_cache;
- $this->memory_cache = new StudipMemoryCache();
+ $this->memory_cache = new MemoryCache();
}
/**
@@ -39,47 +41,55 @@ class StudipCacheWrapper implements StudipCache
$this->actual_cache->flush();
}
+ public static function getDisplayName(): string
+ {
+ return static::class;
+ }
+
+ public function getStats(): array
+ {
+ return $this->actual_cache->getStats();
+ }
+
+ public static function getConfig(): array
+ {
+ return [];
+ }
+
/**
- * @inheritdoc
+ * @inheritDoc
*/
- public function read($arg)
+ public function getItem(string $key): CacheItemInterface
{
- $cached = $this->memory_cache->read($arg);
- if ($cached !== false) {
+ $cached = $this->memory_cache->getItem($key);
+ if ($cached->isHit()) {
return $cached;
}
- $cached = $this->actual_cache->read($arg);
- if ($cached !== false) {
- $this->memory_cache->write($arg, $cached, self::DEFAULT_MEMORY_EXPIRATION);
+ $cached = $this->actual_cache->getItem($key);
+ if ($cached->isHit()) {
+ $this->memory_cache->save($cached);
}
return $cached;
}
/**
- * @inheritdoc
+ * @inheritDoc
*/
- public function write($name, $content, $expires = self::DEFAULT_EXPIRATION)
+ public function hasItem(string $key): bool
{
- if ($this->actual_cache->write($name, $content, $expires)) {
- return $this->memory_cache->write($name, $content, $expires);
- } else {
- return false;
- }
- }
-
- public static function getDisplayName(): string
- {
- return static::class;
- }
-
- public function getStats(): array
- {
- return $this->actual_cache->getStats();
+ return $this->actual_cache->hasItem($key);
}
- public static function getConfig(): array
+ /**
+ * @inheritDoc
+ */
+ public function save(CacheItemInterface $item): bool
{
- return [];
+ if ($this->actual_cache->save($item)) {
+ return $this->memory_cache->save($item);
+ } else {
+ return false;
+ }
}
}
diff --git a/lib/classes/calendar/CalendarScheduleModel.php b/lib/classes/calendar/CalendarScheduleModel.php
index 468b7e2..0aeadee 100644
--- a/lib/classes/calendar/CalendarScheduleModel.php
+++ b/lib/classes/calendar/CalendarScheduleModel.php
@@ -623,7 +623,7 @@ class CalendarScheduleModel
SET visible = 0
WHERE seminar_id = ? AND user_id = ? AND metadate_id = ?");
} else {
- $stmt = DBManager::get()->prepare("INSERT INTO schedule_seminare
+ $stmt = DBManager::get()->prepare("INSERT IGNORE INTO schedule_seminare
(seminar_id, user_id, metadate_id, visible)
VALUES(?, ?, ?, 0)");
}
diff --git a/lib/classes/calendar/EventData.class.php b/lib/classes/calendar/EventData.php
index 95e89b0..95e89b0 100644
--- a/lib/classes/calendar/EventData.class.php
+++ b/lib/classes/calendar/EventData.php
diff --git a/lib/classes/calendar/EventSource.interface.php b/lib/classes/calendar/EventSource.php
index 48506d7..48506d7 100644
--- a/lib/classes/calendar/EventSource.interface.php
+++ b/lib/classes/calendar/EventSource.php
diff --git a/lib/classes/calendar/ICalendarExport.class.php b/lib/classes/calendar/ICalendarExport.php
index ce50f87..d8d1af7 100644
--- a/lib/classes/calendar/ICalendarExport.class.php
+++ b/lib/classes/calendar/ICalendarExport.php
@@ -1,6 +1,6 @@
<?php
/**
- * ICalendarExport.class.php
+ * ICalendarExport.php
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
@@ -55,7 +55,7 @@ class ICalendarExport
(`calendar_dates`.`begin` <= :end
AND `calendar_dates`.`end` >= :begin)
OR (`calendar_dates`.`repetition_type` != 'SINGLE'
- AND (`calendar_dates`.`repetition_end` >= :end
+ AND (`calendar_dates`.`repetition_end` >= :begin
OR `calendar_dates`.`repetition_end` = 0)
AND `calendar_dates`.`begin` < :end))",
[
@@ -76,20 +76,7 @@ class ICalendarExport
if ($this->time === 0) {
$this->time = time();
}
- $dates = CourseDate::findBySql(
- "LEFT JOIN `seminar_user`
- ON `termine`.`range_id` = `seminar_user`.`Seminar_id`
- WHERE
- `seminar_user`.`user_id` = :user_id
- AND `seminar_user`.`bind_calendar` = 1
- AND (`termine`.`date` <= :end
- AND `termine`.`end_time` >= :begin)",
- [
- ':user_id' => $user_id,
- ':begin' => $start->getTimestamp(),
- ':end' => $end->getTimestamp(),
- ]
- );
+ $dates = CalendarCourseDate::getEvents($start, $end, $user_id);
$ical = '';
foreach ($dates as $date) {
$ical .= $this->writeICalEvent($this->prepareCourseDate($date));
@@ -102,20 +89,7 @@ class ICalendarExport
if ($this->time === 0) {
$this->time = time();
}
- $dates = CourseExDate::findBySql(
- "LEFT JOIN `seminar_user`
- ON `ex_termine`.`range_id` = `seminar_user`.`Seminar_id`
- WHERE
- `seminar_user`.`user_id` = :user_id
- AND `seminar_user`.`bind_calendar` = 1
- AND (`ex_termine`.`date` <= :end
- AND `ex_termine`.`end_time` >= :begin)",
- [
- ':user_id' => $user_id,
- ':begin' => $start->getTimestamp(),
- ':end' => $end->getTimestamp(),
- ]
- );
+ $dates = CalendarCourseExDate::getEvents($start, $end, $user_id);
$ical = '';
foreach ($dates as $date) {
$ical .= $this->writeICalEvent($this->prepareCourseDate($date));
@@ -124,10 +98,10 @@ class ICalendarExport
}
/**
- * @param CalendarDate | CourseExDate $date
- * @return array
+ * @param CalendarDate $date The calendar date to export.
+ * @return array Calendar date data prepared for export.
*/
- public function prepareCalendarDate($date): array
+ public function prepareCalendarDate(CalendarDate $date): array
{
return [
'SUMMARY' => $date->title,
@@ -155,8 +129,8 @@ class ICalendarExport
}
/**
- * @param CalendarDate | CourseExDate $date
- * @return array
+ * @param CourseDate | CourseExDate $date The course date to export.
+ * @return array Course date data prepared for export.
*/
public function prepareCourseDate($date): array
{
@@ -165,10 +139,13 @@ class ICalendarExport
if ($date instanceof CourseExDate) {
$summary .= ' ' . _('(fällt aus)');
$categories = '';
+ $description = $date->content;
+ } else {
+ $description = implode("\n", $date->topics->pluck('title'));
}
return [
'SUMMARY' => $summary,
- 'DESCRIPTION' => '',
+ 'DESCRIPTION' => $description,
'LOCATION' => $date->getRoomName(),
'CATEGORIES' => $categories,
'LAST-MODIFIED' => $date->chdate,
@@ -286,7 +263,7 @@ class ICalendarExport
case 'DUE':
case 'RECURRENCE-ID':
if (array_key_exists('VALUE', $params)) {
- if ($params['VALUE'] == 'DATE') {
+ if ($params['VALUE'] === 'DATE') {
$value = $this->_exportDate($value);
} else {
$value = $this->_exportDateTime($value);
@@ -299,12 +276,12 @@ class ICalendarExport
break;
case 'EXDATE':
- if (array_key_exists('VALUE', $params)) {
+ if (array_key_exists('VALUE', $params) && $params['VALUE'] === 'DATE') {
$value = $this->exportExDate($value);
} else {
- $value = $this->exportExDateTime($value);
+ $value = $this->exportExDateTime($value, $exdate_time);
+ $params_str = ';TZID=Europe/Berlin';
}
- $params_str = ';TZID=Europe/Berlin';
break;
// Integer fields
@@ -338,7 +315,7 @@ class ICalendarExport
// Recursion fields
case 'EXRULE':
case 'RRULE':
- if ($value['type'] !== 'SINGLE') {
+ if ($value['type'] !== 'SINGLE' && $value['type'] !== '') {
$value = $this->_exportRecurrence($value);
}
break;
@@ -381,8 +358,8 @@ class ICalendarExport
public function _exportDateTime($value, $utc = false)
{
$date_time = new DateTime();
- $date_time->setTimestamp($value);
- //transform local time in UTC
+ $date_time->setTimestamp(intval($value));
+ //transform local time to UTC
if ($utc) {
$tz_utc = new DateTimeZone('UTC');
$date_time->setTimezone($tz_utc);
@@ -427,7 +404,7 @@ class ICalendarExport
$value['offset'] = '-1';
}
- if ($value['count']) {
+ if ($value['count'] > 1) {
unset($value['expire']);
}
@@ -438,7 +415,7 @@ class ICalendarExport
$rrule[] = 'FREQ=' . $r_value;
break;
case 'expire':
- if ($r_value < CalendarDate::NEVER_ENDING)
+ if ($r_value)
$rrule[] = 'UNTIL=' . $this->_exportDateTime($r_value, true);
break;
case 'interval':
@@ -470,7 +447,9 @@ class ICalendarExport
$rrule[] = 'BYMONTH=' . $r_value;
break;
case 'count':
- $rrule[] = 'COUNT=' . $r_value;
+ if ($r_value > 1) {
+ $rrule[] = 'COUNT=' . $r_value;
+ }
break;
}
}
@@ -501,35 +480,40 @@ class ICalendarExport
return implode(',', $wdays);
}
+
/**
* Formats dates of exception.
*
- * @param string $value Unix timestamps as csv list.
+ * @param string $value Date values (Y-m-d) as csv list.
* @return string The formatted Exceptions.
*/
public function exportExDate(string $value): string
{
- $exdates = [];
- $date_times = explode(',', $value);
- foreach ($date_times as $date_time) {
- $exdates[] = $this->_exportDate($date_time);
+ $ex_dates = [];
+ $dates = explode(',', $value);
+ foreach ($dates as $date) {
+ $ex_datetime = $date . ' 12:00:00';
+ $ex_date = DateTime::createFromFormat('Y-m-d H:i:s', $ex_datetime);
+ $ex_dates[] = $this->_exportDate($ex_date->getTimestamp());
}
- return implode(',', $exdates);
+
+ return implode(',', $ex_dates);
}
/**
* Formats date times of exception.
*
- * @param string $value Unix timestamps as csv list.
+ * @param string $value Date values (Y-m-d) as csv list.
+ * @param int $begin Start date of event as unix timestamp.
* @return string The formatted Exceptions.
*/
- public function exportExDateTime(string $value): string
+ public function exportExDateTime(string $value, int $begin): string
{
$ex_dates = [];
- $ex_date_times = explode(',', $value);
- foreach ($ex_date_times as $ex_date_time) {
- $date_time = new DateTime();
- $date_time->setTimestamp($ex_date_time);
+ $dates = explode(',', $value);
+ foreach ($dates as $date) {
+ $ex_datetime = $date . date(' H:i:s', $begin);
+ $date_time = DateTime::createFromFormat('Y-m-d H:i:s', $ex_datetime);
$ex_dates[] = $date_time->format('Ymd\THis');
}
return implode(',', $ex_dates);
diff --git a/lib/classes/calendar/ICalendarImport.class.php b/lib/classes/calendar/ICalendarImport.php
index e78696d..e78696d 100644
--- a/lib/classes/calendar/ICalendarImport.class.php
+++ b/lib/classes/calendar/ICalendarImport.php
diff --git a/lib/classes/calendar/Owner.interface.php b/lib/classes/calendar/Owner.php
index a7c2519..a7c2519 100644
--- a/lib/classes/calendar/Owner.interface.php
+++ b/lib/classes/calendar/Owner.php
diff --git a/lib/classes/cas/CAS_PGTStorage_Cache.php b/lib/classes/cas/CAS_PGTStorage_Cache.php
index 284b591..61ce9fa 100644
--- a/lib/classes/cas/CAS_PGTStorage_Cache.php
+++ b/lib/classes/cas/CAS_PGTStorage_Cache.php
@@ -43,7 +43,7 @@ class CAS_PGTStorage_Cache extends CAS_PGTStorage_AbstractStorage
*/
public function write($pgt, $pgt_iou)
{
- $cache = StudipCacheFactory::getCache();
+ $cache = \Studip\Cache\Factory::getCache();
$cache_key = 'pgtiou/' . $pgt_iou;
return $cache->write($cache_key, $pgt);
}
@@ -58,7 +58,7 @@ class CAS_PGTStorage_Cache extends CAS_PGTStorage_AbstractStorage
*/
public function read($pgt_iou)
{
- $cache = StudipCacheFactory::getCache();
+ $cache = \Studip\Cache\Factory::getCache();
$cache_key = 'pgtiou/' . $pgt_iou;
$pgt = $cache->read($cache_key);
$cache->expire($cache_key);
diff --git a/lib/classes/coursewizardsteps/AdvancedBasicDataWizardStep.php b/lib/classes/coursewizardsteps/AdvancedBasicDataWizardStep.php
index 780f837..cd33fd3 100644
--- a/lib/classes/coursewizardsteps/AdvancedBasicDataWizardStep.php
+++ b/lib/classes/coursewizardsteps/AdvancedBasicDataWizardStep.php
@@ -36,7 +36,7 @@ class AdvancedBasicDataWizardStep extends BasicDataWizardStep
}
// Load template from step template directory.
- $factory = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'].'/app/views/course/wizard/steps');
+ $factory = new Flexi\Factory($GLOBALS['STUDIP_BASE_PATH'].'/app/views/course/wizard/steps');
$template = $factory->open('advancedbasicdata/index');
$template = $this->setupTemplateAttributes($template, $values, $stepnumber, $temp_id);
diff --git a/lib/classes/coursewizardsteps/BasicDataWizardStep.php b/lib/classes/coursewizardsteps/BasicDataWizardStep.php
index 4f424e8..d47c4f3 100644
--- a/lib/classes/coursewizardsteps/BasicDataWizardStep.php
+++ b/lib/classes/coursewizardsteps/BasicDataWizardStep.php
@@ -28,7 +28,7 @@ class BasicDataWizardStep implements CourseWizardStep
public function getStepTemplate($values, $stepnumber, $temp_id)
{
// Load template from step template directory.
- $factory = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'] . '/app/views/course/wizard/steps');
+ $factory = new Flexi\Factory($GLOBALS['STUDIP_BASE_PATH'] . '/app/views/course/wizard/steps');
if (!empty($values[__CLASS__]['studygroup'])) {
$tpl = $factory->open('basicdata/index_studygroup');
$values[__CLASS__]['lecturers'][$GLOBALS['user']->id] = 1;
@@ -356,9 +356,9 @@ class BasicDataWizardStep implements CourseWizardStep
htmlReady(get_title_for_status('dozent', 1, $values['coursetype']))
);
}
- if (!$values['lecturers'][$GLOBALS['user']->id] && !$GLOBALS['perm']->have_perm('admin')) {
+ if (empty($values['lecturers'][$GLOBALS['user']->id]) && !$GLOBALS['perm']->have_perm('admin')) {
if (Config::get()->DEPUTIES_ENABLE) {
- if (!$values['deputies'][$GLOBALS['user']->id]) {
+ if (empty($values['deputies'][$GLOBALS['user']->id])) {
$errors[] = sprintf(
_('Sie selbst müssen entweder als %s oder als Vertretung eingetragen sein.'),
htmlReady(get_title_for_status('dozent', 1, $values['coursetype']))
@@ -620,7 +620,10 @@ class BasicDataWizardStep implements CourseWizardStep
}
}
-
+ } else {
+ foreach ($indices as $index) {
+ $values[$index] = $values[$index] ?? '';
+ }
}
return $values;
diff --git a/lib/classes/coursewizardsteps/LVGroupsWizardStep.php b/lib/classes/coursewizardsteps/LVGroupsWizardStep.php
index 22f11ea..c651714 100644
--- a/lib/classes/coursewizardsteps/LVGroupsWizardStep.php
+++ b/lib/classes/coursewizardsteps/LVGroupsWizardStep.php
@@ -13,7 +13,7 @@
* @category Stud.IP
*/
-require_once dirname(__FILE__) . '/../StudipLvgruppeSelection.class.php';
+require_once dirname(__FILE__) . '/../StudipLvgruppeSelection.php';
class LVGroupsWizardStep implements CourseWizardStep
{
@@ -36,10 +36,10 @@ class LVGroupsWizardStep implements CourseWizardStep
$course_start_time = $values[$step_one_class]['start_time'];
// We only need our own stored values here.
- $values = $values[__CLASS__];
+ $values = $values[__CLASS__] ?? [];
// Load template from step template directory.
- $factory = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'] . '/app/views/course/wizard/steps');
+ $factory = new Flexi\Factory($GLOBALS['STUDIP_BASE_PATH'] . '/app/views/course/wizard/steps');
$tpl = $factory->open('lvgroups/index');
$tpl->set_attribute('values', $values);
@@ -53,9 +53,12 @@ class LVGroupsWizardStep implements CourseWizardStep
}
}
- $selection_details = $values['lvgruppe_selection']['area_details'];
+ $selection_details = $values['lvgruppe_selection']['area_details'] ?? null;
- if ($_SESSION[__CLASS__]['course_start_time'] != $course_start_time) {
+ if (
+ isset($_SESSION[__CLASS__]['course_start_time'])
+ && $_SESSION[__CLASS__]['course_start_time'] != $course_start_time
+ ) {
// don't store previously opened nodes
// because we get in trouble if the semester has changed
$open_nodes = [];
@@ -65,15 +68,15 @@ class LVGroupsWizardStep implements CourseWizardStep
$_SESSION[__CLASS__]['course_start_time'] = $course_start_time;
- $tpl->set_attribute('open_lvg_nodes', $open_nodes);
- $tpl->set_attribute('selection', $selection);
- $tpl->set_attribute('selection_details', $selection_details);
- $tpl->set_attribute('tree', $lvgtree->getRootItem()->getChildren());
+ $tpl->open_lvg_nodes = $open_nodes;
+ $tpl->selection = $selection;
+ $tpl->selection_details = $selection_details;
+ $tpl->tree = $lvgtree->getRootItem()->getChildren();
- $tpl->set_attribute('ajax_url', $values['ajax_url'] ?: URLHelper::getLink('dispatch.php/course/wizard/ajax'));
- $tpl->set_attribute('no_js_url', $values['no_js_url'] ?: 'dispatch.php/course/wizard/forward/'.$stepnumber.'/'.$temp_id);
- $tpl->set_attribute('stepnumber', $stepnumber);
- $tpl->set_attribute('temp_id', $temp_id);
+ $tpl->ajax_url = !empty($values['ajax_url']) ? $values['ajax_url'] : URLHelper::getLink('dispatch.php/course/wizard/ajax');
+ $tpl->no_js_url = !empty($values['no_js_url']) ? $values['no_js_url'] : URLHelper::getURL('dispatch.php/course/wizard/forward/'.$stepnumber.'/'.$temp_id);
+ $tpl->stepnumber = $stepnumber;
+ $tpl->temp_id = $temp_id;
return $tpl->render();
}
@@ -223,7 +226,7 @@ class LVGroupsWizardStep implements CourseWizardStep
continue;
}
- $factory = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'] . '/app/views');
+ $factory = new Flexi\Factory($GLOBALS['STUDIP_BASE_PATH'] . '/app/views');
$html = $factory->render('course/wizard/steps/lvgroups/lvgroup_searchentry', compact('area'));
$data = [
'id' => $area->id,
@@ -285,7 +288,7 @@ class LVGroupsWizardStep implements CourseWizardStep
'Studiengang']);
$pathes = ModuleManagementModelTreeItem::getPathes($trails);
- $factory = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'] . '/app/views');
+ $factory = new Flexi\Factory($GLOBALS['STUDIP_BASE_PATH'] . '/app/views');
$html = $factory->render('course/lvgselector/entry_trails',
compact('area', 'pathes'));
@@ -305,7 +308,7 @@ class LVGroupsWizardStep implements CourseWizardStep
$mvvid = explode('-', $id);
$area = Lvgruppe::find($mvvid[0]);
- $factory = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'] . '/app/views');
+ $factory = new Flexi\Factory($GLOBALS['STUDIP_BASE_PATH'] . '/app/views');
$html = $factory->render('course/wizard/steps/lvgroups/lvgroup_entry', ['area' => $area, 'locked' => false, 'course_id' => '']);
$data = [
diff --git a/lib/classes/coursewizardsteps/StudyAreasWizardStep.php b/lib/classes/coursewizardsteps/StudyAreasWizardStep.php
index f81ce41..eac7e37 100644
--- a/lib/classes/coursewizardsteps/StudyAreasWizardStep.php
+++ b/lib/classes/coursewizardsteps/StudyAreasWizardStep.php
@@ -28,9 +28,9 @@ class StudyAreasWizardStep implements CourseWizardStep
public function getStepTemplate($values, $stepnumber, $temp_id)
{
// We only need our own stored values here.
- $values = $values[get_class($this)];
+ $values = $values[get_class($this)] ?? [];
// Load template from step template directory.
- $factory = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'].'/app/views/course/wizard/steps');
+ $factory = new Flexi\Factory($GLOBALS['STUDIP_BASE_PATH'].'/app/views/course/wizard/steps');
$tpl = $factory->open('studyareas/index');
if (!empty($values['studyareas'])) {
$tree = $this->buildPartialSemTree(StudipStudyArea::backwards(StudipStudyArea::findMany($values['studyareas'])));
diff --git a/lib/classes/exportdocument/ExportDocument.interface.php b/lib/classes/exportdocument/ExportDocument.php
index 27aaee2..27aaee2 100644
--- a/lib/classes/exportdocument/ExportDocument.interface.php
+++ b/lib/classes/exportdocument/ExportDocument.php
diff --git a/lib/classes/exportdocument/ExportPDF.class.php b/lib/classes/exportdocument/ExportPDF.php
index 8645f5b..f676c45 100644
--- a/lib/classes/exportdocument/ExportPDF.class.php
+++ b/lib/classes/exportdocument/ExportPDF.php
@@ -1,7 +1,7 @@
<?php
# Lifter010: TODO
/**
- * ExportPDF.class.php - create and export or save a pdf with simple HTML-Data
+ * ExportPDF.php - create and export or save a pdf with simple HTML-Data
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
@@ -115,12 +115,15 @@ class ExportPDF extends TCPDF implements ExportDocument
// Fetch headers from url, handle possible redirects
do {
$headers = get_headers($url, true, get_default_http_stream_context($url));
+ if (!$headers) {
+ break;
+ }
list(, $status) = explode(' ', $headers[0]);
$url = $headers['Location'] ?? $headers['location'] ?? $url;
} while (in_array($status, [300, 301, 302, 303, 305, 307]));
- $status = $status ?: 404;
+ $status = $status ?? 404;
// Replace image with link on error (and not internal), otherwise return sainitized
// url
diff --git a/lib/classes/forms/Captcha.php b/lib/classes/forms/Captcha.php
new file mode 100644
index 0000000..c01b702
--- /dev/null
+++ b/lib/classes/forms/Captcha.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Studip\Forms;
+
+use CaptchaChallenge;
+
+/**
+ * The Text class represents a part of a form that displays a captcha.
+ */
+class Captcha extends Fieldset
+{
+ private CaptchaInput $captcha_input;
+
+ public function __construct()
+ {
+ parent::__construct(_('Bitte bestätigen Sie, dass Sie kein Roboter sind'));
+
+ $captchaInput = new CaptchaInput('altcha', $this->legend, null);
+ $captchaInput->setStoringFunction(function (string $payload) {
+ $json = CaptchaChallenge::decodePayload($payload);
+
+ CaptchaChallenge::create([
+ 'salt' => $json['salt'],
+ 'number' => $json['number'],
+ ]);
+ });
+ $this->addInput($captchaInput);
+ }
+}
diff --git a/lib/classes/forms/CaptchaInput.php b/lib/classes/forms/CaptchaInput.php
new file mode 100644
index 0000000..6476f87
--- /dev/null
+++ b/lib/classes/forms/CaptchaInput.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Studip\Forms;
+
+use CaptchaChallenge;
+use URLHelper;
+
+/**
+ * The Text class represents a part of a form that displays a captcha.
+ */
+final class CaptchaInput extends Input
+{
+ public function hasValidation(): bool
+ {
+ return true;
+ }
+
+ public function getValidationCallback(): callable
+ {
+ return fn($value) => \CaptchaChallenge::validatePayload($value);
+ }
+
+ public function render(): string
+ {
+ return sprintf(
+ '<captcha-input challenge-url="%s" v-model="%s" auto="onload"></captcha-input>',
+ URLHelper::getLink('dispatch.php/captcha/challenge', [], true),
+ htmlReady($this->name)
+ );
+ }
+
+ public function renderWithCondition(): string
+ {
+ return $this->render();
+ }
+
+
+}
diff --git a/lib/classes/forms/Form.php b/lib/classes/forms/Form.php
index fa0422e..9c22c4f 100644
--- a/lib/classes/forms/Form.php
+++ b/lib/classes/forms/Form.php
@@ -297,6 +297,8 @@ class Form extends Part
\PageLayout::postSuccess($this->success_message);
}
page_close();
+ //This indicates that the form has been stored successfully.
+ echo "STUDIPFORM_STORE_SUCCESS";
die();
}
}
@@ -309,7 +311,7 @@ class Form extends Part
//verify the user input:
$output = [];
foreach ($this->getAllInputs() as $input) {
- if ($input->validate) {
+ if ($input->hasValidation()) {
$callback = $input->getValidationCallback();
$value = $this->getStorableValueFromRequest($input);
$valid = $callback($value, $input);
@@ -317,7 +319,7 @@ class Form extends Part
$output[$input->getName()] = [
'name' => $input->getName(),
'label' => $input->getTitle(),
- 'error' => $callback($value, $input)
+ 'error' => $valid,
];
}
}
@@ -396,7 +398,7 @@ class Form extends Part
$stored = 0;
foreach ($this->getAllInputs() as $input) {
- if ($input->validate) {
+ if ($input->hasValidation()) {
$callback = $input->getValidationCallback();
$value = $this->getStorableValueFromRequest($input);
$valid = $callback($value, $input);
@@ -450,7 +452,7 @@ class Form extends Part
/**
* Returns all the Part objects like Fieldsets as an array.
- * @return array
+ * @return Part[]
*/
public function getParts() : array
{
@@ -492,7 +494,7 @@ class Form extends Part
/**
* Renders the whole form as a string.
* @return string
- * @throws \Flexi_TemplateNotFoundException
+ * @throws \Flexi\TemplateNotFoundException
*/
public function render()
{
diff --git a/lib/classes/forms/Input.php b/lib/classes/forms/Input.php
index 9d2ad32..ef506e9 100644
--- a/lib/classes/forms/Input.php
+++ b/lib/classes/forms/Input.php
@@ -150,12 +150,17 @@ abstract class Input
}
/**
- * Returns the value of this input.
- * @return null
+ * Returns the value of this input. If $this->value is a callable this->getValue() returns the computed result.
+ * @return mixed
*/
public function getValue()
{
- return $this->value;
+ if (is_callable($this->value)) {
+ $callable = $this->value;
+ return $callable();
+ } else {
+ return $this->value;
+ }
}
/**
diff --git a/lib/classes/forms/Part.php b/lib/classes/forms/Part.php
index fdca8f5..779cab7 100644
--- a/lib/classes/forms/Part.php
+++ b/lib/classes/forms/Part.php
@@ -139,7 +139,7 @@ abstract class Part
/**
* Recursively returns all Input elements attached to this Part object or any child Parts.
- * @return array
+ * @return Input[]
*/
public function getAllInputs()
{
diff --git a/lib/classes/globalsearch/GlobalSearchCourses.php b/lib/classes/globalsearch/GlobalSearchCourses.php
index bf29ca0..9de5535 100644
--- a/lib/classes/globalsearch/GlobalSearchCourses.php
+++ b/lib/classes/globalsearch/GlobalSearchCourses.php
@@ -225,9 +225,9 @@ class GlobalSearchCourses extends GlobalSearchModule implements GlobalSearchFull
array_map(
function ($lecturer, $index) use ($search, $course) {
if ($index < 3) {
- return '<a href="' . URLHelper::getURL('dispatch.php/profile', ['username' => $lecturer->username]) . '">' . self::mark($lecturer->getUserFullname(), $search) . '</a>';
+ return self::mark($lecturer->getUserFullname(), $search);
} else if ($index == 3) {
- return '<a href="' . URLHelper::getURL('dispatch.php/course/details/index/' . $course->id) . '">... (' . _('mehr') . ') </a>';
+ return '... (' . _('mehr') . ')';
}
},
$lecturers,
diff --git a/lib/classes/globalsearch/GlobalSearchCourseware.php b/lib/classes/globalsearch/GlobalSearchCourseware.php
index d4c53b1..de069fe 100644
--- a/lib/classes/globalsearch/GlobalSearchCourseware.php
+++ b/lib/classes/globalsearch/GlobalSearchCourseware.php
@@ -142,7 +142,7 @@ class GlobalSearchCourseware extends GlobalSearchModule implements GlobalSearchF
'description' => $description,
'url' => $pageData['url'],
'img' => $structural_element->image ? $structural_element->getImageUrl() : Icon::create('courseware')->asImagePath(),
- 'additional' => '<a href="' . htmlReady($pageData['originUrl']) . '" title="' . htmlReady($pageData['originName']) . '">' . htmlReady($pageData['originName']) . '</a>',
+ 'additional' => htmlReady($pageData['originName']),
'date' => $date->format('d.m.Y H:i'),
'structural-element-id' => $structural_element->id,
'expand' => null
diff --git a/lib/classes/globalsearch/GlobalSearchMessages.php b/lib/classes/globalsearch/GlobalSearchMessages.php
index d1a91a5..5f64b40 100644
--- a/lib/classes/globalsearch/GlobalSearchMessages.php
+++ b/lib/classes/globalsearch/GlobalSearchMessages.php
@@ -79,11 +79,7 @@ class GlobalSearchMessages extends GlobalSearchModule
if ($user) {
$username = $user->getFullName();
- $additional = sprintf(
- '<a href="%s">%s</a>',
- URLHelper::getLink('dispatch.php/profile', ['username' => $user->username]),
- self::mark($user->getFullName(), $search)
- );
+ $additional = self::mark($user->getFullName(), $search);
}
}
diff --git a/lib/classes/globalsearch/GlobalSearchMyCourses.php b/lib/classes/globalsearch/GlobalSearchMyCourses.php
index f8f2f11..6558f78 100644
--- a/lib/classes/globalsearch/GlobalSearchMyCourses.php
+++ b/lib/classes/globalsearch/GlobalSearchMyCourses.php
@@ -162,9 +162,9 @@ class GlobalSearchMyCourses extends GlobalSearchModule
array_map(
function ($lecturer, $index) use ($search, $course) {
if ($index < 3) {
- return '<a href="' . URLHelper::getURL('dispatch.php/profile', ['username' => $lecturer->username]) . '">' . self::mark($lecturer->getUserFullname(), $search) . '</a>';
+ return self::mark($lecturer->getUserFullname(), $search);
} else if ($index == 3) {
- return '<a href="' . URLHelper::getURL('dispatch.php/course/details/index/' . $course->id) . '">... (' . _('mehr') . ') </a>';
+ return '... (' . _('mehr') . ')';
}
},
$lecturers,
diff --git a/lib/classes/globalsearch/GlobalSearchUsers.php b/lib/classes/globalsearch/GlobalSearchUsers.php
index fb8677a..458f098 100644
--- a/lib/classes/globalsearch/GlobalSearchUsers.php
+++ b/lib/classes/globalsearch/GlobalSearchUsers.php
@@ -86,7 +86,7 @@ class GlobalSearchUsers extends GlobalSearchModule implements GlobalSearchFullte
['username' => $user->username],
true
),
- 'additional' => '<a href="' . URLHelper::getLink('dispatch.php/profile', ['username' => $user->username]) . '">' . self::mark($user->username, $search) . '</a>',
+ 'additional' => self::mark($user->username, $search),
'expand' => self::getSearchURL($search),
'img' => Avatar::getAvatar($user->id)->getUrl(Avatar::MEDIUM),
];
diff --git a/lib/classes/helpbar/Helpbar.php b/lib/classes/helpbar/Helpbar.php
index cfa98a3..c4abfc4 100644
--- a/lib/classes/helpbar/Helpbar.php
+++ b/lib/classes/helpbar/Helpbar.php
@@ -253,7 +253,8 @@ class Helpbar extends WidgetContainer
// add wiki link and remove it from navigation
$this->addLink(
_('Weiterführende Hilfe'),
- format_help_url(PageLayout::getHelpKeyword()), Icon::create('link-extern', 'info_alt'),
+ PageLayout::getHelpUrl(),
+ Icon::create('link-extern', Icon::ROLE_INFO_ALT),
'_blank',
['rel' => 'noopener noreferrer']
);
@@ -264,7 +265,7 @@ class Helpbar extends WidgetContainer
$template = $GLOBALS['template_factory']->open('helpbar/helpbar');
$template->widgets = $this->widgets;
$template->open = $this->open;
- $template->tour_data = $tour_data;
+ $template->tour_data = $tour_data ?? null;
$content = $template->render();
}
diff --git a/lib/classes/librarysearch/LibraryDocument.class.php b/lib/classes/librarysearch/LibraryDocument.php
index 4c90e0b4..c1d297e 100644
--- a/lib/classes/librarysearch/LibraryDocument.class.php
+++ b/lib/classes/librarysearch/LibraryDocument.php
@@ -14,7 +14,6 @@
* @since 4.6
*/
-
/**
* This class represents a document from a library.
*/
@@ -322,8 +321,8 @@ class LibraryDocument
$doc->csl_data = $data['csl_data'];
$doc->datafields = $data['datafields'];
$doc->search_params = $data['search_params'];
- $doc->catalog = $data['catalog'];
- $doc->opac_link = $data['opac_link'];
+ $doc->catalog = $data['catalog'] ?? null;
+ $doc->opac_link = $data['opac_link'] ?? null;
return $doc;
}
@@ -386,12 +385,12 @@ class LibraryDocument
/**
- * @returns Flexi_Template A template containing information about the
+ * @returns Flexi\Template A template containing information about the
* the document.
*/
public function getInfoTemplate($format = 'short')
{
- $factory = new Flexi_TemplateFactory(
+ $factory = new Flexi\Factory(
$GLOBALS['STUDIP_BASE_PATH'] . '/templates/library/'
);
$template = $factory->open('library_document_info');
diff --git a/lib/classes/librarysearch/LibraryResultParser.interface.php b/lib/classes/librarysearch/LibraryResultParser.php
index eae45a4..eae45a4 100644
--- a/lib/classes/librarysearch/LibraryResultParser.interface.php
+++ b/lib/classes/librarysearch/LibraryResultParser.php
diff --git a/lib/classes/librarysearch/LibrarySearch.class.php b/lib/classes/librarysearch/LibrarySearch.php
index 473eacf..473eacf 100644
--- a/lib/classes/librarysearch/LibrarySearch.class.php
+++ b/lib/classes/librarysearch/LibrarySearch.php
diff --git a/lib/classes/librarysearch/LibrarySearchManager.class.php b/lib/classes/librarysearch/LibrarySearchManager.php
index ee9dc75..ee9dc75 100644
--- a/lib/classes/librarysearch/LibrarySearchManager.class.php
+++ b/lib/classes/librarysearch/LibrarySearchManager.php
diff --git a/lib/classes/librarysearch/resultparsers/BASELibraryResultParser.class.php b/lib/classes/librarysearch/resultparsers/BASELibraryResultParser.php
index b92628f..b92628f 100644
--- a/lib/classes/librarysearch/resultparsers/BASELibraryResultParser.class.php
+++ b/lib/classes/librarysearch/resultparsers/BASELibraryResultParser.php
diff --git a/lib/classes/librarysearch/resultparsers/K10PlusLibraryResultParser.class.php b/lib/classes/librarysearch/resultparsers/K10PlusLibraryResultParser.php
index b4b3a20..b4b3a20 100644
--- a/lib/classes/librarysearch/resultparsers/K10PlusLibraryResultParser.class.php
+++ b/lib/classes/librarysearch/resultparsers/K10PlusLibraryResultParser.php
diff --git a/lib/classes/librarysearch/resultparsers/MarcxmlLibraryResultParser.class.php b/lib/classes/librarysearch/resultparsers/MarcxmlLibraryResultParser.php
index 8017f3d..8017f3d 100644
--- a/lib/classes/librarysearch/resultparsers/MarcxmlLibraryResultParser.class.php
+++ b/lib/classes/librarysearch/resultparsers/MarcxmlLibraryResultParser.php
diff --git a/lib/classes/librarysearch/resultparsers/SRULibraryResultParser.class.php b/lib/classes/librarysearch/resultparsers/SRULibraryResultParser.php
index fed92f3..fed92f3 100644
--- a/lib/classes/librarysearch/resultparsers/SRULibraryResultParser.class.php
+++ b/lib/classes/librarysearch/resultparsers/SRULibraryResultParser.php
diff --git a/lib/classes/librarysearch/searchmodules/BASELibrarySearch.class.php b/lib/classes/librarysearch/searchmodules/BASELibrarySearch.php
index e0408e7..e0408e7 100644
--- a/lib/classes/librarysearch/searchmodules/BASELibrarySearch.class.php
+++ b/lib/classes/librarysearch/searchmodules/BASELibrarySearch.php
diff --git a/lib/classes/librarysearch/searchmodules/K10PlusZentralLibrarySearch.class.php b/lib/classes/librarysearch/searchmodules/K10PlusZentralLibrarySearch.php
index 9660cb4..9660cb4 100644
--- a/lib/classes/librarysearch/searchmodules/K10PlusZentralLibrarySearch.class.php
+++ b/lib/classes/librarysearch/searchmodules/K10PlusZentralLibrarySearch.php
diff --git a/lib/classes/librarysearch/searchmodules/SRULibrarySearch.class.php b/lib/classes/librarysearch/searchmodules/SRULibrarySearch.php
index b94869a..b94869a 100644
--- a/lib/classes/librarysearch/searchmodules/SRULibrarySearch.class.php
+++ b/lib/classes/librarysearch/searchmodules/SRULibrarySearch.php
diff --git a/lib/classes/restapi/ConsumerPermissions.php b/lib/classes/restapi/ConsumerPermissions.php
deleted file mode 100644
index 8fc2252..0000000
--- a/lib/classes/restapi/ConsumerPermissions.php
+++ /dev/null
@@ -1,212 +0,0 @@
-<?php
-namespace RESTAPI;
-use DBManager, PDO;
-
-/**
- * REST API routing permissions
- *
- * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
- * @license GPL 2 or later
- * @since Stud.IP 3.0
- * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0.
- */
-class ConsumerPermissions
-{
- /**
- * Create a permission object (for a certain consumer).
- * Permissions object will be cached for each consumer.
- *
- * @param mixed $consumer_id Id of consumer (optional, defaults to global)
- * @return ConsumerPermissions Returns permissions object
- */
- public static function get($consumer_id = null)
- {
- static $cache = [];
- if (!isset($cache[$consumer_id])) {
- $cache[$consumer_id] = new self($consumer_id);
- }
-
- return $cache[$consumer_id];
- }
-
- private $consumer_id;
- private $permissions = [];
-
- /**
- * Creates the actual permission object (for a certain consumer).
- *
- * @param mixed $consumer_id Id of consumer (optional, defaults to global)
- */
- private function __construct($consumer_id = null)
- {
- $this->consumer_id = $consumer_id;
-
- // Init with global permissions
- $this->loadPermissions('global', true);
-
- // Specific consumers permissions?
- if ($consumer_id) {
- $this->loadPermissions($consumer_id, false);
- }
- }
-
- /**
- * Defines whether access if allowed for the current consumer to the
- * passed route via the passed method.
- *
- * @param String $route_id Route template (hash)
- * @param String $method HTTP method
- * @param mixed $granted Granted state (PHP'ish boolean)
- * @param bool $overwrite May values be overwritten
- * @return bool Indicates if value could be changed.
- */
- public function set($route_id, $method, $granted, $overwrite = false)
- {
- // If route_id is not an md5 hash, convert it
- if (!preg_match('/^[0-9a-f]{32}$/', $route_id)) {
- $route_id = md5($route_id);
- }
-
- if (!isset($this->permissions[$route_id])) {
- // Skip if not globally set and not allowed to overwrite
- if (!$overwrite) {
- return false;
- }
- $this->permissions[$route_id] = [];
- }
-
- // overwrite only if globally allowed
- if (!$overwrite && empty($this->permissions[$route_id][$method])) {
- return false;
- }
-
- $this->permissions[$route_id][$method] = (bool) $granted;
-
- return true;
- }
-
- /**
- * Convenience method for activating all routes in a route map.
- *
- * @param \RESTAPI\RouteMap $routemap RouteMap to activate
- */
- public function activateRouteMap(RouteMap $routemap)
- {
- foreach ($routemap->getRoutes() as $method => $routes) {
- foreach (array_keys($routes) as $route) {
- $this->set($route, $method, true, true);
- }
- }
-
- $this->store();
- }
-
- /**
- * Removes stored permissions for a given route and method.
- *
- * @param String $route_id Route template
- * @param String $method HTTP method
- * @return bool
- */
- public function remove($route_id, $method)
- {
- if (!isset($this->permissions[$route_id][$method])) {
- return false;
- }
-
- unset($this->permissions[$route_id][$method]);
-
- if (count($this->permissions[$route_id]) === 0) {
- unset($this->permissions[$route_id]);
- }
-
- return true;
- }
-
- /**
- * Convenience method for deactivating all routes in a route map.
- *
- * @param \RESTAPI\RouteMap $routemap RouteMap to activate
- */
- public function deactivateRouteMap(RouteMap $routemap)
- {
- foreach ($routemap->getRoutes() as $method => $routes) {
- foreach (array_keys($routes) as $route) {
- $this->remove($route, $method);
- }
- }
-
- $this->store();
- }
-
- /**
- * Loads permissions for passed consumer.
- *
- * @param String $consumer_id Id of the consumer in question
- * @param bool $overwrite May values be overwritten
- * @return ConsumerPermissions Returns instance of self to allow chaining
- */
- protected function loadPermissions($consumer_id, $overwrite = false)
- {
- $query = "SELECT route_id, method, granted
- FROM api_consumer_permissions
- WHERE consumer_id = IFNULL(:consumer_id, 'global')";
- $statement = DBManager::get()->prepare($query);
- $statement->bindValue(':consumer_id', $consumer_id);
- $statement->execute();
- $permissions = $statement->fetchAll(PDO::FETCH_ASSOC);
-
- // Init with global permissions
- foreach ($permissions as $permission) {
- extract($permission);
-
- $this->set($route_id, $method, $granted, $overwrite);
- }
-
- return $this;
- }
-
- /**
- * Checks if access to passed route via passed method is allowed for
- * the current consumer.
- *
- * @param String $route Route template
- * @param String $method HTTP method
- * @return bool Indicates whether access is allowed
- */
- public function check($route, $method)
- {
- $route_id = md5($route);
-
- return isset($this->permissions[$route_id][$method])
- && $this->permissions[$route_id][$method];
- }
-
- /**
- * Stores the set permissions.
- *
- * @return bool Returns true if permissions were stored successfully
- */
- public function store()
- {
- $result = true;
-
- $query = "INSERT INTO api_consumer_permissions (route_id, consumer_id, method, granted)
- VALUES (:route, IFNULL(:consumer_id, 'global'), :method, :granted)
- ON DUPLICATE KEY UPDATE granted = VALUES(granted)";
- $statement = DBManager::get()->prepare($query);
- $statement->bindValue(':consumer_id', $this->consumer_id);
-
- foreach ($this->permissions as $route_id => $methods) {
- $statement->bindParam(':route', $route_id);
- foreach ($methods as $method => $granted) {
- $statement->bindParam(':method', $method);
- $granted = (int) !empty($granted);
- $statement->bindParam(':granted', $granted);
- $result = $result && $statement->execute();
- }
- }
-
- return $result;
- }
-}
diff --git a/lib/classes/restapi/Response.php b/lib/classes/restapi/Response.php
deleted file mode 100644
index 4417979..0000000
--- a/lib/classes/restapi/Response.php
+++ /dev/null
@@ -1,164 +0,0 @@
-<?php
-namespace RESTAPI;
-
-/**
- * Response class for the rest api
- *
- * @author <mlunzena@uos.de>
- * @license GPL 2 or later
- * @since Stud.IP 3.0
- * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0.
- */
-class Response implements \ArrayAccess
-{
- public $body, $status, $headers;
-
- /**
- * Constructor, sets vital information if provided.
- *
- * @param String $body Body contents of the response, optional,
- * defaults to empty string
- * @param int $status HTTP status code, optional, defaults to 200
- * @param Array $headers HTTP headers, optional, defaults to no headers
- */
- public function __construct($body = '', $status = 200, $headers = [])
- {
- $this->body = $body;
- $this->status = (int) $status;
- $this->headers = (array) $headers;
- }
-
- /**
- * Detects whether the response status is of success type (HTTP status 2xx)
- *
- * @return bool True if status is of success type, false otherwise
- */
- public function isSuccess()
- {
- return 200 <= $this->status && $this->status <= 299;
- }
-
- /**
- * Finishes the response with the given response renderer.
- *
- * @param Renderer\DefaultRenderer $content_renderer Used response renderer,
- * only applied if body is
- * not a callable closure
- */
- public function finish($content_renderer)
- {
- if (!is_callable($this->body)) {
- $content_renderer->render($this);
- }
- }
-
- /**
- * Sends the response.
- */
- public function output()
- {
- if (isset($this->status)) {
- if (mb_strpos(PHP_SAPI, 'cgi') === 0) {
- $this->sendHeader(sprintf('Status: %d %s', $this->status, $this->reason()));
- } else {
- $this->sendHeader(sprintf('HTTP/1.1 %d %s', $this->status, $this->reason()));
- }
- }
-
- foreach ($this->headers as $k => $v) {
- $this->sendHeader("$k: $v", false, $this->status);
- }
-
- if (is_callable($this->body)) {
- call_user_func($this->body);
- } else {
- echo $this->body;
- }
- }
-
- /**
- * Internally used function to actually send headers
- *
- * @param string the HTTP header
- * @param bool optional; TRUE if previously sent header should be
- * replaced - FALSE otherwise (default)
- * @param integer optional; the HTTP response code
- *
- * @return void
- */
- public function sendHeader($header, $replace = FALSE, $status = NULL) {
- if (isset($status)) {
- header($header, $replace, $status);
- }
- else {
- header($header, $replace);
- }
- }
-
- /**
- * Returns the reason phrase of this response according to RFC2616.
- *
- * @return string the reason phrase for this response's status
- */
- public function reason() {
- $reason = [
- 100 => 'Continue', 'Switching Protocols',
- 200 => 'OK', 'Created', 'Accepted', 'Non-Authoritative Information',
- 'No Content', 'Reset Content', 'Partial Content',
- 300 => 'Multiple Choices', 'Moved Permanently', 'Found', 'See Other',
- 'Not Modified', 'Use Proxy', '(Unused)', 'Temporary Redirect',
- 400 => 'Bad Request', 'Unauthorized', 'Payment Required','Forbidden',
- 'Not Found', 'Method Not Allowed', 'Not Acceptable',
- 'Proxy Authentication Required', 'Request Timeout', 'Conflict',
- 'Gone', 'Length Required', 'Precondition Failed',
- 'Request Entity Too Large', 'Request-URI Too Long',
- 'Unsupported Media Type', 'Requested Range Not Satisfiable',
- 'Expectation Failed',
- 500 => 'Internal Server Error', 'Not Implemented', 'Bad Gateway',
- 'Service Unavailable', 'Gateway Timeout',
- 'HTTP Version Not Supported'];
-
- return isset($reason[$this->status]) ? $reason[$this->status] : '';
- }
-
- // array access methods for headers
-
- /**
- * @todo Add bool return type when Stud.IP requires PHP8 minimal
- */
- #[ReturnTypeWillChange]
- public function offsetExists($offset)
- {
- return isset($this->headers[$offset]);
- }
-
- /**
- * @param $offset
- * @return mixed
- *
- * @todo Add mixed return type when Stud.IP requires PHP8 minimal
- */
- #[\ReturnTypeWillChange]
- public function offsetGet($offset)
- {
- return @$this->headers[$offset];
- }
-
- /**
- * @todo Add void return type when Stud.IP requires PHP8 minimal
- */
- #[ReturnTypeWillChange]
- public function offsetSet($offset, $value)
- {
- $this->headers[$offset] = $value;
- }
-
- /**
- * @todo Add void return type when Stud.IP requires PHP8 minimal
- */
- #[ReturnTypeWillChange]
- public function offsetUnset($offset)
- {
- unset($this->headers[$offset]);
- }
-}
diff --git a/lib/classes/restapi/RouteMap.php b/lib/classes/restapi/RouteMap.php
deleted file mode 100644
index b8ad2f4..0000000
--- a/lib/classes/restapi/RouteMap.php
+++ /dev/null
@@ -1,1060 +0,0 @@
-<?php
-namespace RESTAPI;
-
-use Config;
-use Request;
-use gossi\docblock\Docblock;
-
-/**
- * RouteMaps define and group routes to resources.
- *
- * Instances of RouteMaps are registered with the RESTAPI\Router to
- * participate in the routing business.
- *
- * A RouteMap defines at least one handler method which has to be
- * annotated with one of these annotations correlating to HTTP request
- * methods:
- *
- * @code
- * / * *
- * * An example handler method
- * *
- * * @get /foo
- * * @post /bar/:id
- * * @put /baz/:id/:other_id
- * * @delete /
- * * /
- * public function anyMethodName($id, $other_id = null) {}
- * @endcode
- *
- * By default, all API routes are unaccessible for nobody users.
- * To explicitly allow access for nobody users, add the allow_nobody
- * tag to the handler method's doc block. Example:
- *
- * @code
- * / * *
- * * Another example handler method
- * *
- * * @get /foo
- * *
- * * @allow_nobody
- * * /
- * @endcode
- *
- * As soon as the Router matches a HTTP request to a handler defined
- * in a RouteMap, it calls RouteMap::init to initialize it and
- * especially the instance field `$this->response` of type
- * RESTAPI\Response. You do not call RouteMap::init on your own.
- *
- * After the router has initialized this RouteMap, the router tries to
- * call a method `before` of this signature:
- *
- * @code
- * public function before(Router $router, Array $handler, Array $parameters);
- * @endcode
- *
- * The parameter `$handler` is a callable (as in function is_callable)
- * consisting of the instance of this RouteMap and the name of a
- * method of this instance. You may change the values of this array to
- * redirect to another handler.
- *
- * The parameter `$parameters` is an associative array whose keys
- * correlate to the placeholders in the matched URI template. The
- * values are the actual values of that placeholders in regard to the
- * HTTP request.
- *
- *
- * After calling RouteMap::before control is transfered to the actual
- * handler method. The values of the placeholders in the URI template
- * of the annotation are send as arguments to the handler.
- *
- * Example: We have got this handler method defined:
- *
- * @code
- * / * *
- * * @get /foo/:id/bar/:other_id
- * * /
- * public function fooHandler($id, $other_id) {
- * }
- * @endcode
- *
- * The router receives a request like this: `http://[..]/foo/1/bar/2`
- * and matches it to our `fooHandler` which is then called something
- * like that:
- *
- * @code
- * $result = $routeMap->fooHandler(1, 2);
- * @endcode
- *
- * In your handler methods you have to process the input and return
- * some output data, which is then rendered in an appropriate way
- * after negotiating the content format in the Router.
- *
- * Thus the return value of your handler method becomes the body of
- * the HTTP response.
- *
- *
- * The RouteMap class defines several methods to ease up your work
- * with the HTTP specifica.
- *
- * The methods RouteMap::status, RouteMap::headers and RouteMap::body
- * correlate to the components of a HTTP response.
- *
- * There are helpers for returning paginated collections, see
- * RouteMap::paginated.
- *
- * If you encounter an error or have to stop further processing, see
- * methods RouteMap::halt, RouteMap::error and RouteMap::notFound.
- *
- * These methods are \a DISRUPTIVE as they immediately stop the control
- * flow in your handler:
- *
- * @code
- * public function fooHandler($id)
- * {
- * // do something
- *
- * $this->halt();
- *
- * // this line will never be reached
- * }
- * @endcode
- *
- * If you want to simply send a redirection response (HTTP status code
- * of 302 or 303), you may find calling RouteMap::redirect helpful.
- *
- * To generate a URL to a handler, use RouteMap::url
- *
- * When you find the need to return the content of a file, please see
- * RouteMap::sendFile which will help you with streaming it to the
- * client. For custom streaming just return a Closure from your
- * handler method.
- *
- * There are several other methods which you may find useful each
- * matching a HTTP header:
- *
- * - RouteMap::contentType
- * - RouteMap::etag
- * - RouteMap::expires
- * - RouteMap::cacheControl
- * - RouteMap::lastModified
- *
- * You can access the data sent in the body of the current HTTP
- * request using the `$this->data` instance variable.
- *
- * - If the request was of Content-Type `application/json`, the
- * body of the request is decoded using `json_decode`.
- * - If the request was of Content-Type
- * `application/x-www-form-urlencoded`, the body of the request is
- * decoded using `parse_str`.
- * - Otherwise the request will not be parsed and `$this->data` will
- * just contain the raw string.
- *
- * NOTE: The result of the described parsing will always contain
- * strings encoded in windows-1252. If the original body
- * was UTF-8 encoded, it is automatically re-encoded to windows-1252.
- *
- * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
- * @author <mlunzena@uos.de>
- * @license GPL 2 or later
- * @since Stud.IP 3.0
- * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0.
- */
-abstract class RouteMap
-{
- protected $router;
- protected $route;
- protected $data = null;
- protected $response;
-
- /**
- * Internal property which is used by RouteMap::paginated and
- * contains everything about a paginated collection.
- */
- protected $pagination = false;
-
- /**
- * The offset into a RouteMap::paginated collection as requested
- * by the client.
- */
- protected $offset;
-
- /**
- * The limit of a RouteMap::paginated collection as requested
- * by the client.
- */
- protected $limit;
-
- /**
- * Constructor of the route map. Initializes neccessary offset and limit
- * parameters for pagination.
- */
- public function __construct()
- {
- $this->offset = Request::int('offset', 0);
- $this->limit = Request::int('limit', Config::get()->ENTRIES_PER_PAGE);
- }
-
- /**
- * Initializes the route map by binding it to a router and passing in
- * the current route.
- *
- * @param Router $router Router to bind this route map to
- * @param array $route The matched route out of Router::matchRoute;
- * an array with keys 'handler', 'conditions' and
- * 'source'
- */
- public function init($router, $route)
- {
- $this->router = $router;
- $this->route = $route;
- $this->response = new Response();
-
- if ($mediaType = $this->getRequestMediaType()) {
- $this->data = $this->parseRequestBody($mediaType);
- }
- }
-
- /**
- * Marks this chunk of data as a slice of a larger data set with
- * a sum of "total" entries.
- *
- * @param mixed $data Chunk of data (should be sliced according
- * to current offset and limit parameters).
- * @param int $total The total number of data entries in the
- * according set.
- * @param array $uri_params Neccessary parameters when generating uris
- * for the current route.
- * @param array $query_params Optional query parameters.
- */
- public function paginated($data, $total, $uri_params = [], $query_params = [])
- {
- $uri = $this->url($this->route['uri_template']->inject($uri_params), $query_params);
-
- $this->paginate($uri, $total);
- return $this->collect($data);
- }
-
-
- /**
- * Low level method for paginating collections. You better use
- * RouteMap::paginated instead of this.
- *
- * Set the pagination data used by the RouteMap::collect.
- *
- * @param String $uri_format
- * @param int $total
- * @param mixed $offset
- * @param mixed $limit
- *
- * @return Routemap Returns instance of self to allow chaining
- */
- public function paginate($uri_format, $total, $offset = null, $limit = null)
- {
- $total = (int)$total;
- $offset = (int)($offset ?: $this->offset ?: 0);
- $limit = (int)($limit ?: $this->limit);
-
- $this->pagination = compact('uri_format', 'total', 'offset', 'limit');
-
- return $this;
- }
-
- /**
- * Low level method for paginating collections. You better use
- * RouteMap::paginated instead of this.
- *
- * Adjusts the result set to return a collection. A collection consists
- * of the passed data array and the associated pagination information
- * if available.
- *
- * Be aware that the passed data has to be already sliced according to
- * the pagination information.
- *
- * @param array $data Actual dataset
- * @return array Collection "object"
- */
- public function collect($data)
- {
- $collection = [
- 'collection' => $data
- ];
- if (is_array($this->pagination)) {
- extract($this->pagination);
-
- $offset = $offset - $offset % $limit;
- $max = ($total % $limit)
- ? $total - $total % $limit
- : $total - $limit;
-
- $pagination = compact('total', 'offset', 'limit');
- if ($total > $limit) {
- $links = [];
-
- foreach ([
- 'first' => 0,
- 'previous' => max(0, $offset - $limit),
- 'next' => min($max, $offset + $limit),
- 'last' => $max]
- as $key => $offset)
- {
- $links[$key] = \URLHelper::getURL($uri_format, compact('offset', 'limit'));
- }
-
- $pagination['links'] = $links;
- }
- $collection['pagination'] = $pagination;
- }
- return $collection;
- }
-
- /************************/
- /* REQUEST BODY METHODS */
- /************************/
-
- // find the requested media type
- private function getRequestMediaType()
- {
- if (!empty($_SERVER['CONTENT_TYPE'])) {
- $contentTypeParts = preg_split('/\s*[;,]\s*/', $_SERVER['CONTENT_TYPE']);
- return mb_strtolower($contentTypeParts[0]);
- }
- }
-
- // media-types that we know how to process
- private static $mediaTypes = [
- 'application/json' => 'parseJson',
- 'application/x-www-form-urlencoded' => 'parseFormEncoded',
- 'multipart/form-data' => 'parseMultipartFormdata'
- ];
-
- // cache the request body
- private static $_request_body;
-
- // reads the HTTP request body
- private function parseRequestBody($mediaType)
- {
- // read it only once
- if (!isset(self::$_request_body)) {
- self::$_request_body = file_get_contents('php://input');
- }
-
- if (isset(self::$mediaTypes[$mediaType])) {
- $result = call_user_func([__CLASS__, self::$mediaTypes[$mediaType]], self::$_request_body);
- if ($result) {
- return $result;
- }
- }
- return self::$_request_body;
- }
-
- // strategy to decode JSON strings
- private static function parseJson($input)
- {
- return json_decode($input, true);
- }
-
- // strategy to decode form encoded strings
- private static function parseFormEncoded($input)
- {
- parse_str($input, $result);
- return $result;
- }
-
- // strategy to decode a multipart message. Used for file-uploads.
- private static function parseMultipartFormdata($input)
- {
-
- $data = [];
- if (Request::isPost()) {
- foreach ($_POST as $key => $value) {
- $data[$key] = $value;
- }
- $data['_FILES'] = $_FILES;
- return $data;
- }
- $boundary = self::getMultipartBoundary();
- if (!$boundary) {
- return $data;
- }
- $input = explode("--".$boundary, $input);
-
- array_pop($input);
- array_shift($input);
-
- foreach ($input as $part) {
- $part = ltrim($part, "\r\n");
- [$head, $body] = explode("\r\n\r\n", $part, 2);
-
- $tmpheaders = $headers = [];
- foreach (explode("\r\n", $head) as $headline) {
- if (preg_match('/^[^\s]/', $headline)) {
- $lineIsHeader = preg_match('/([^:]+):\s*(.*)$/', $headline, $matches);
- if ($lineIsHeader) {
- $tmpheaders[] = ['index' => mb_strtolower(trim($matches[1])), 'value' => trim($matches[2])];
- }
- } else {
- //noch zur letzten Zeile hinzuzählen
- end($tmpheaders);
- $lastkey = key($tmpheaders);
- $tmpheaders[$lastkey]['value'] .= " ".mb_substr($headline, 1);
- }
- }
- foreach ($tmpheaders as $header) {
- $headers[$header['index']] = $header['value'];
- }
-
- $contentType = "";
- if (isset($headers['content-type'])) {
- preg_match("/^([^;\s]*)/", $headers['content-type'], $matches);
- $contentType = mb_strtolower($matches[1]);
- }
- switch ($headers["transfer-encoding"]) {
- case "quoted-printable":
- $body = quoted_printable_decode($body);
- break;
- case "base64":
- $body = base64_decode(preg_replace("/(\r?\n|\r)/", "", trim($body)));
- break;
- case "7bit":
- case "8bit":
- default:
- //nothing to do
- }
- $matches = [];
- preg_match("/name=([^;\s]*)/i", $headers['content-disposition'], $matches);
- $name = str_replace(["'", '"'], '', $matches[1]);
- if (!$contentType) {
- $data[$name] = mb_substr($body, 0, mb_strlen($body) - 2);
- } else {
- switch ($contentType) {
- case 'application/json':
- $data = array_merge($data, self::parseJson($body));
- break;
- case 'application/x-www-form-urlencoded':
- $data = array_merge($data, self::parseFormEncoded($body));
- break;
- default:
- $matches = [];
- preg_match("/filename=([^;\s]*)/i", $headers['content-disposition'], $matches);
- if (!$matches[1]) {
- preg_match('/filename=([^;\s]*)/i', $headers['content-type'], $matches);
- }
- $filename = str_replace(["'", '"'], '', $matches[1]);
- $tmp_name = $GLOBALS['TMP_PATH']."/uploadfile_".md5(uniqid());
- $handle = fopen($tmp_name, 'wb');
- $filesize = fwrite($handle, $body, (mb_strlen($body) - 2));
- fclose($handle);
- $data['_FILES'][$name] = [
- 'name' => $filename,
- 'type' => $contentType,
- 'tmp_name' => $tmp_name,
- 'size' => $filesize
- ];
- }
- }
- }
- return $data;
- }
-
- private static function getMultipartBoundary()
- {
- if ($contentType = $_SERVER['CONTENT_TYPE']) {
- foreach (preg_split('/\s*[;,]\s*/', $contentType) as $part) {
- if (mb_strtolower(mb_substr($part, 0, 8)) === "boundary") {
- $part = explode("=", $part);
- return $part[1];
- }
- }
- }
- return null;
- }
-
-
- /**
- * Set the HTTP status of the current response.
- *
- * @param integer $status the HTTP status of the response
- */
- public function status($status)
- {
- $this->response->status = $status;
- }
-
- /**
- * Set multiple response headers of the current response by
- * merging them with already set ones.
- *
- * @code
- * $routemap->headers(array('X-example' => "yep"));
- * @endcode
- *
- * @param array $headers the headers to set
- *
- * @return array the headers of the current response
- */
- public function headers($headers = [])
- {
- if (sizeof($headers)) {
- $this->response->headers = array_merge($this->response->headers, $headers);
- }
- return $this->response->headers;
- }
-
- /**
- * Set the HTTP body of the current response.
- *
- * @param string $body the body to send back
- */
- public function body($body)
- {
- $this->response->body = $body;
- }
-
-
- /**
- * Set the Content-Type of the HTTP response given a mime type and
- * optionally further parameters as discusses in RFC 2616 14.17.
- *
- * If no charset is given, it defaults to Stud.IP's 'windows-1252'.
- *
- * Examples:
- *
- * @code
- * // results in "Content-Type: image/gif"
- * $this->contentType('image/gif);
- *
- * // results in "Content-Type: text/html;charset=ISO-8859-4"
- * $this->contentType('text/html;charset=ISO-8859-4');
- *
- * // results in "Content-Type: text/html;charset=ISO-8859-4"
- * $this->contentType('text/html', array('charset' => 'ISO-8859-4'));
- *
- * // results in "Content-type: multipart/byteranges; boundary=THIS_STRING_SEPARATES"
- * $this->contentType('multipart/byteranges', array('boundary' => 'THIS_STRING_SEPARATES'));
- *
- * @endcode
- *
- * @param string $mime_type a string describing a MIME type like 'application/json'
- * @param array $params optional parameters as described above
- */
- public function contentType($mime_type, $params = [])
- {
- if (!isset($params['charset'])) {
- $params['charset'] = 'utf-8';
- }
-
- if (mb_strpos($mime_type, 'charset') !== FALSE) {
- unset($params['charset']);
- }
-
- if (sizeof($params)) {
- $mime_type .= mb_strpos($mime_type, ';') !== FALSE ? ', ' : ';';
- $ps = [];
- foreach ($params as $k => $v) {
- $ps[] = $k . '=' . $v;
- }
- $mime_type .= join(', ', $ps);
- }
-
- $this->response['Content-Type'] = $mime_type;
- }
-
- /**
- * (Nice) sugar for calling RouteMap::halt and therefore
- * as \a DISRUPTIVE. Code after calling RouteMap::error will not
- * be evaluated.
- *
- * @see RouteMap::halt
- *
- * @param integer $status a number indicating the HTTP status
- * code; probably something 4xx or 5xx-ish
- * @param string $body optional; the body of the HTTP response
- *
- */
- public function error($status, $body = null)
- {
- $this->halt($status, [], $body);
- }
-
-
- /**
- * Sets the HTTP response's Etag header and halts, if the incoming
- * HTTP request was a matching conditional GET using an
- * 'If-None-Match' header. Thus it is a possibly \a DISRUPTIVE
- * method as it will stop evaluation in that case and send a '304
- * Not Modified'.
- *
- * Detail: If the request contains an If-Match or If-None-Match
- * header set to `*`, a RouteMap assumes a match on safe
- * (e.g. GET) and idempotent (e.g. PUT) requests. (In those cases
- * it thinks that the resource already exists and therefore
- * matches a wildcard.). This can be changed by passing an
- * appropriate value for the `$new_resource` parameter.
-
- * Details of this can be found in RFC 2616 14.24 and 14.26
- *
- * @param string $value an identifier uniquely identifying the
- * current state of a resource
- * @param bool $strong_etag optional; indicates whether the etag
- * is a weak or strong (which is the
- * default) cache validator. Have a look
- * at the RFC for details.
- * @param bool $new_resource optional; a way to tell the RouteMap
- * that this is a new or existing
- * resource. See above.
- */
-
- public function etag($value, $strong_etag = true, $new_resource = null)
- {
- // Before touching this code, please double check RFC 2616
- // 14.24 and 14.26.
-
- if (!isset($new_resource)) {
- $new_resource = Request::isPost();
- }
-
- $value = '"' . $value . '"';
- if (!$strong_etag) {
- $value = 'W/' . $value;
- }
- $this->response['ETag'] = $value;
-
- if ($this->response->isSuccess() || $this->response->status === 304) {
- if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $this->etagMatches($_SERVER['HTTP_IF_NONE_MATCH'], $new_resource)) {
- $this->halt($this->isRequestSafe() ? 304 : 412);
- }
- if (isset($_SERVER['HTTP_IF_MATCH'])
- && !$this->etagMatches($_SERVER['HTTP_IF_MATCH'], $new_resource)) {
- $this->halt(412);
- }
- }
- }
-
- // Helper method checking if a ETag value list includes the current ETag.
- private function etagMatches($list, $new_resource)
- {
- if ($list === '*') {
- return !$new_resource;
- }
-
- return in_array($this->response['ETag'],
- preg_split('/\s*,\s*/', $list));
- }
-
- // Helper method checking if the request is safe
- private function isRequestSafe()
- {
- $method = Request::method();
- return $method === 'GET' or $method === 'HEAD' or $method === 'OPTIONS' or $method === 'TRACE';
- }
-
- /**
- * This sets the `Expires` header and the `Cache-Control`
- * directive `max-age`.
- *
- * Amount is an integer number of seconds in the future indicating
- * when the response should be considered "stale". The
- * `$cache_control` parameter is passed to RouteMap#cacheControl
- * along with the automatically generated `max_age` directive.
- *
- * @param int $amount an integer specifying the number of seconds
- * this resource will go stale.
- * @param array $cache_control optional; more directives for
- * RouteMap::cacheControl which is always
- * automatically called using the computed max_age
- */
- public function expires($amount, $cache_control = [])
- {
- $time = time() + $amount;
- $max_age = $amount;
-
- $cache_control[] = "max-age=$max_age";
- $this->cacheControl($cache_control);
-
- $this->response['Expires'] = $this->httpDate($time);
- }
-
- /**
- * This sets the Cache-Control header of the HTTP response.
- *
- * Example:
- *
- * @code
- * $this->cacheControl(array('public', 'must-revalidate'));
- * @endcode
- *
- * @param array $values an array containing Cache-Control
- * directives.
- */
- public function cacheControl($values)
- {
- if (is_array($values) && sizeof($values)) {
- $this->response['Cache-Control'] = join(', ', $values);
- }
- }
-
- /**
- * This very important method stops further execution of your
- * code. You may specify a status code, headers and the body of
- * the resulting response. As the name implies, this method is \a
- * DISRUPTIVE and will not return.
- *
- * @code
- * // stops any further code of a route
- * $this->halt();
- *
- * // you may specify an HTTP status
- * $this->halt(409):
- *
- * // you may specify the HTTP response's body
- * $this->halt('my ethereal body')
- *
- * // or even both
- * $this->halt(100, 'Yes, pleazze!')
- *
- * // giving headers
- * $this->halt(417, array('Content-Type' => 'x-not-a-cat'), 'Cats only!')
- * @endcode
- *
- * This method is called by every single \a DISRUPTIVE method.
- *
- * @param integer $status optional; the response's status code
- * @param array $headers optional; (additional) header lines
- * which get merged with already set headers
- * @param string $body optional; the response's body
- */
- public function halt(/* [status], [headers], [body] */)
- {
- $args = func_get_args();
- $result = [];
-
- $constraints = [
- 'status' => 'is_int',
- 'headers' => 'is_array',
- 'body' => function ($i) { return isset($i); } // #existy
- ];
- foreach ($constraints as $state => $constraint) {
- if ($constraint(current($args))) {
- call_user_func([$this, $state], array_shift($args));
- }
- }
-
- throw new RouterHalt($this->response);
- }
-
- /**
- * This method sets the Last-Modified header of the HTTP response
- * and halts on matching conditional GET requests. Thus this
- * method is \a DISRUPTIVE in certain circumstances.
- *
- * You have to give an integer typed timestamp (in seconds since
- * epoch) to specify the data of the last modification to the
- * requested resource.
- *
- * If the current HTTP request contains an `If-Modified-Since`
- * header, its value is compared to the specified `$time`
- * parameter. Unless the header's value is sooner than the given
- * `$time`, further execution is precluded and the RouteMap
- * returns with a '304 Not Modified'.
- *
- * @param integer $time a timestamp described in seconds since epoch
- */
- public function lastModified($time)
- {
-
- $this->response['Last-Modified'] = $this->httpDate($time);
-
- if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
- return;
- }
-
- if ($this->response->status === 200
- && isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
- // compare based on seconds since epoch
- $since = $this->httpdate($_SERVER['HTTP_IF_MODIFIED_SINCE']);
- if ($since >= (int) $time) {
- $this->halt(304);
- }
- }
-
- if (($this->response->isSuccess() || $this->response->status === 412)
- && isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE'])) {
-
- // compare based on seconds since epoch
- $since = $this->httpdate($_SERVER['HTTP_IF_UNMODIFIED_SINCE']);
-
- if ($since < (int) $time) {
- $this->halt(412);
- }
- }
- }
-
- private function httpDate($timestamp)
- {
- return gmdate('D, d M Y H:i:s \G\M\T', (int) $timestamp);
- }
-
- /**
- * Halts execution and returns a '404 Not Found' response.
- *
- * Sugar for calling RouteMap::error(404) and therefore
- * \a DISRUPTIVE. Code after calling RouteMap::notFound will
- * not be evaluated.
- *
- * @see RouteMap::error
- * @see RouteMap::halt
- *
- * @param string $body optional; the body of the HTTP response
- */
- public function notFound($body = null)
- {
- $this->halt(404, $body);
- }
-
- /**
- * Stops your code and redirects to the URL provided. This method
- * is \a DISRUPTIVE like RouteMap#halt
- *
- * In addition to the URL you may provide the status code,
- * (additional) headers and a request body as you would when
- * calling RouteMap#halt.
- *
- * @code
- * $this->redirect('/foo', 201, array('X-Some-Header' => 1234), 'and even a body');
- * @endcode
- *
- * @see RouteMap::halt
- *
- * @param string $url the URL to redirect to; it will be filtered
- * using RouteMap#url, so you may call it with
- * those nice and small strings used in the
- * annotations
- * @param mixed $args optional; any combinations of the three
- * parameters as in RouteMap::halt
- */
- public function redirect($url, $args = null)
- {
- $this->status($_SERVER["SERVER_PROTOCOL"] === 'HTTP/1.1' && !Request::isGet() ? 303 : 302);
- $this->response['Location'] = $this->url($url);
-
- $args = array_slice(func_get_args(), 1);
- call_user_func_array([$this, 'halt'], $args);
- }
-
-
- /**
- * Stops execution of your code and starts sending the specified
- * file. This method is \a DISRUPTIVE.
- *
- * Using the `$opts` parameter you may specify the file's mime
- * content type, sending an appropriate 'Content-Type' header, and
- * you may specify the 'Content-Disposition' of the file transfer.
- *
- * Example:
- *
- * @code
- * $this->sendFile('/tmp/c29tZSB0ZXh0', array(
- * 'type' => 'image/png',
- * 'disposition' => 'inline',
- * 'filename' => 'cutecats.png'));
- * @endcode
- *
- * @param string $_path the filesystem path to the file to send
- * @param array $opts optional; specify the content type,
- * disposition and filename
- */
- public function sendFile($_path, $opts = [])
- {
- $path = realpath($_path);
-
- if (!file_exists($path)) {
- $this->notFound('File to send does not exist');
- }
-
- if (isset($opts['type'])) {
- $this->contentType($opts['type']);
- } else if (!isset($this->response['Content-Type'])) {
- $this->contentType(get_mime_type($path));
- }
-
- if ($opts['disposition'] === 'attachment' || isset($opts['filename'])) {
- $this->response['Content-Disposition'] = 'attachment; ';
- $filename = $opts['filename'] ?: $path;
- $this->response['Content-Disposition'] .= encode_header_parameter('filename', basename($filename));
- }
-
- elseif ($opts['disposition'] === 'inline') {
- $this->response['Content-Disposition'] = 'inline';
- }
-
- // TODO add HTTP 'Range' support
-
- $size = filesize($path);
- $this->response['Content-Length'] = $size;
-
- // End all potential output buffers
- while (ob_get_level() > 0) {
- ob_end_clean();
- }
-
- // Send file
- $this->halt(200, $this->response->headers, function () use ($path) {
- readfile($path);
- });
- }
-
-
- /**
- * Generate a URL to a given handler using a URL fragment and URL
- * parameters.
- *
- * Example:
- * @code
- * // result in something like "/some/path/api.php/course/123/members?status=student"
- * $this->url('course/123/members', array('status' => 'student'));
- * @endcode
- *
- * @param string $addr a URL fragment to a handler
- * @param array $url_params optional; URL parameters to add to
- * the generated URL
- *
- * @return string the resulting URL
- */
- public function url($addr, $url_params = null)
- {
- $addr = ltrim($addr, '/');
- return \URLHelper::getURL("api.php/$addr", $url_params, true);
- }
-
- /**
- * A `vsprintf` like variant to the RouteMap::url method.
- *
- * Example:
- * @code
- * // results in "[...]/api.php/foo/some_id?status=student"
- * $this->urlf("foo/%s", array("some_id"), array('status' => 'student'));
- * @endcode
- *
- * @param string $addr_f a URL fragment to a handler
- * containing sprintf-ish format sequences
- * @param array $format_params values to fill into the format markers
- * @param array $url_params optional; URL parameters to add to
- * the generated URL
- *
- * @return string the resulting URL
- */
-
- public function urlf($addr_f, $format_params, $url_params = null)
- {
- if (!is_array($format_params)) {
- $format_params = [$format_params];
- }
- return $this->url(vsprintf($addr_f, $format_params), $url_params);
- }
-
- /**
- * Returns a list of all the routes this routemap provides.
- *
- * @param string $http_method Return only the routes for this specific
- * http method (optional)
- *
- * @return array of all routes grouped by method
- */
- public function getRoutes($http_method = null)
- {
- $ref = new \ReflectionClass($this);
-
- if ($ref->getDocComment()) {
- $docblock = new Docblock($ref);
- $class_conditions = $this->extractConditions($docblock);
- } else {
- $class_conditions = [];
- }
-
-
- // Create result array by creating an associative array from all
- // supported methods as keys
- $routes = array_fill_keys(Router::getSupportedMethods(), []);
-
- // Restrict routes to given http method (if given)
- if ($http_method !== null) {
- $routes = [$http_method => []];
- }
-
- // Iterate through all methods of the routemap
- foreach ($ref->getMethods( \ReflectionMethod::IS_PUBLIC) as $ref_method) {
- // No docblock? Not an api route!
- if (!$ref_method->getDocComment()) {
- continue;
- }
-
- // Parse docblock
- $docblock = new Docblock($ref_method);
-
- // No docblock tags? Not an api route!
- if ($docblock->getTags()->isEmpty()) {
- continue;
- }
-
- // Any specific condition to consider?
- $conditions = $this->extractConditions($docblock, $class_conditions);
-
- // Iterate through all possible methods in order to identify
- // any according docblock tags
- $allow_nobody = $docblock->hasTag('allow_nobody');
- foreach (array_keys($routes) as $http_method) {
- if (!$docblock->hasTag($http_method)) {
- //The tag for the current HTTP method cannot be found
- //in the route's DocBlock tags.
- continue;
- }
-
- // Route all defined method and uri template combinations to
- // the according methods of the object.
- foreach ($docblock->getTags($http_method) as $tag) {
- $uri_template = trim($tag->getDescription());
- $routes[$http_method][$uri_template] = [
- 'handler' => [$this, $ref_method->name],
- 'conditions' => $conditions,
- 'description' => trim($docblock->getShortDescription()) ?: false,
- 'allow_nobody' => $allow_nobody
- ];
- }
- }
- }
-
- // Return all routes grouped or just the routes for the wanted method
- return func_num_args() === 1
- ? reset($routes)
- : $routes;
- }
-
- /**
- * Extracts defined conditions from a given docblock.
- *
- * @param Docblock $docblock DocBlock to examine
- * @param array $conditions Optional array of already defined
- * conditions to extend
- * @return array of all extracted conditions with the variable name
- * as key and pattern to match as value
- */
- protected function extractConditions($docblock, $conditions = [])
- {
- foreach ($docblock->getTags('condition') as $condition) {
- [$var, $pattern] = explode(' ', $condition->getDescription(), 2);
- $conditions[$var] = $pattern;
- }
-
- return $conditions;
- }
-
- /**
- * Returns the response object
- * @return Response
- */
- public function getResponse(): Response
- {
- return $this->response;
- }
-}
diff --git a/lib/classes/restapi/Router.php b/lib/classes/restapi/Router.php
deleted file mode 100644
index df7a6b9..0000000
--- a/lib/classes/restapi/Router.php
+++ /dev/null
@@ -1,665 +0,0 @@
-<?php
-/** @namespace RESTAPI
- *
- * Im Namensraum RESTAPI sind alle Klassen und Funktionen versammelt,
- * die für die RESTful Web Services von Stud.IP benötigt werden.
- */
-namespace RESTAPI;
-use RESTAPI\Renderer\DefaultRenderer;
-
-/**
- * Die Aufgabe des Routers ist das Anlegen und Auswerten eines
- * Mappings von sogenannten Routen (Tupel aus HTTP-Methode und Pfad)
- * auf Code.
- *
- * Dazu werden zunächst Routen mittels der Funktion
- * Router::registerRoutes registriert.
- *
- * Wenn dann ein HTTP-Request eingeht, kann mithilfe von
- * Router::dispatch und HTTP-Methode bzw. Pfad der zugehörige Code
- * gefunden und ausgeführt werden. Der Router bildet aus dem
- * Rückgabewert des Codes ein Response-Objekt, das er als Ergebnis
- * zurück meldet.
- *
- * @code
- * $router = Router::getInstance();
- *
- * // register a sample Route
- * $router->registerRoutes(new ExampleRoute);
- *
- * // dispatch to therein defined Routes
- * $response = $router->dispatch('/example', 'GET');
- *
- * // render response
- * $response->output();
- *
- * @endcode
- *
- * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
- * @author <mlunzena@uos.de>
- * @license GPL 2 or later
- * @see Inspired by http://blog.sosedoff.com/2009/07/04/simpe-php-url-routing-controller/
- * @since Stud.IP 3.0
- * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0.
- */
-class Router
-{
- // instances are cached here
- protected static $instances = [];
-
- /**
- * Holds the user object of the user that is accessing the API.
- * This is null for nobody users.
- */
- protected $user = null;
-
- /**
- * Returns (and if neccessary, initializes) a (cached) router object for an
- * optional consumer id.
- *
- * @param mixed $consumer_id ID of the consumer (defaults to 'global')
- *
- * @return Router returns the Router instance associated to the
- * consumer ID (or to the 'global' ID)
- */
- public static function getInstance($consumer_id = null)
- {
- $consumer_id = $consumer_id ?: 'global';
-
- if (!isset(self::$instances[$consumer_id])) {
- self::$instances[$consumer_id] = new self($consumer_id);
- }
- return self::$instances[$consumer_id];
- }
-
- // All supported method need to be defined here
- protected static $supported_methods = [
- 'get', 'post', 'put', 'delete', 'patch', 'options', 'head'
- ];
-
- /**
- * Returns a list of all supported methods.
- *
- * @return array of methods as strings
- */
- public static function getSupportedMethods()
- {
- return self::$supported_methods;
- }
-
- // registered routes by method and uri template
- protected $routes = [];
-
- // registered content renderers
- protected $renderers = [];
-
- // identified or forced content renderer
- protected $content_renderer = false;
-
- // default renderer
- protected $default_renderer = false;
-
- // registered conditions
- protected $conditions = [];
-
- // registered descriptions
- protected $descriptions = [];
-
- // registered consumers
- protected $consumers = [];
-
- // associated permissions
- protected $permissions = false;
-
- /**
- * Constructs the router.
- *
- * @param mixed $consumer_id the ID of the consumer this router
- * should associate to
- */
- protected function __construct($consumer_id)
- {
- $this->permissions = ConsumerPermissions::get($consumer_id);
- $this->registerRenderer(new Renderer\DefaultRenderer);
- }
-
- /**
- * Registers a handler for a specific combination of request method
- * and uri template.
- *
- * @param String $request_method expected HTTP request method
- * @param String $uri_template expected URI template, for
- * example: \code "/user/:user_id/events" \endcode
- * @param Array $handler request handler array:
- * \code array($object, "methodName") \endcode
- * @param Array $conditions (optional) an associative
- * array using the name of
- * parameters as keys and regexps
- * as value
- * @param string $source (optional) this denotes the
- * origin of a route. Usually
- * either 'core' or 'plugin', but
- * defaults to 'unknown'.
- * @param bool $allow_nobody Whether the route can be accessed
- * as nobody user (true) or not (false).
- * Defaults to false.
- *
- * @return Router returns itself to allow chaining
- * @throws \Exception if passed HTTP request method is not supported
- */
- public function register($request_method, $uri_template, $handler, $conditions = [], $source = 'unknown', $allow_nobody = false)
- {
- // Normalize method and test whether it's supported
- $request_method = mb_strtolower($request_method);
- if (!in_array($request_method, self::$supported_methods)) {
- throw new \Exception('Method "' . $request_method . '" is not supported.');
- }
-
- // Initialize routes storage for this method if neccessary
- if (!isset($this->routes[$request_method])) {
- $this->routes[$request_method] = [];
- }
-
- // Normalize uri template (always starts with a slash)
- if ($uri_template[0] !== '/') {
- $uri_template = '/' . $uri_template;
- }
-
- // Sanitize conditions
- foreach ($conditions as $var => $pattern) {
- if ($pattern[0] !== $pattern[mb_strlen($pattern) - 1] || ctype_alnum($pattern[0])) {
- $conditions[$var] = '/' . $pattern . '/';
- }
- }
-
- $this->routes[$request_method][$uri_template] = compact(
- 'handler', 'conditions', 'source', 'allow_nobody'
- );
-
- // Return instance to allow chaining
- return $this;
- }
-
- /**
- * Registers the routes defined in a RouteMap instance using
- * docblock annotations (like @get) of its methods.
- *
- * \code
- * $router = \RESTAPI\Router::getInstance();
- *
- * $router->registerRoutes(new ExampleRouteMap());
- * \endcode
- *
- * @param RouteMap $map the RouteMap instance to register
- *
- * @return Router returns itself to allow chaining
- */
- public function registerRoutes(RouteMap $map)
- {
- // Investigate object, define whether it's located in the core system
- // or a plugin, respect any defined class conditions and iterate
- // through it's methods to find any defined route
- $ref = new \ReflectionClass($map);
- $filename = $ref->getFilename();
- $source = mb_strpos($filename, 'plugins_packages') !== false
- ? 'plugin'
- : 'core';
-
- foreach (self::$supported_methods as $http_method) {
- foreach ($map->getRoutes($http_method) as $uri_template => $data) {
- // Register (and describe) route
- $this->register(
- $http_method, $uri_template,
- $data['handler'], $data['conditions'],
- $source,
- $data['allow_nobody']
- );
- if ($data['description']) {
- $this->describe(
- $uri_template,
- $data['description'],
- $http_method
- );
- }
- }
- }
-
- return $this;
- }
-
- /**
- * Describe one or more routes.
- *
- * \code
- * $router = \RESTAPI\Router::getInstance();
- *
- * // describe a single route
- * $router->describe('/foo', 'returns everything about foo', 'get');
- *
- * // describe several routes that use the same path
- * $router->describe('/foo', array(
- * 'get' => 'returns everything about foo',
- * 'put' => 'updates all of foo',
- * 'delete' => 'empty up foo'
- * ));
- *
- * // describe several routes
- * $router->describe(array(
- * '/foo' => array(
- * 'get' => 'returns everything about foo',
- * 'put' => 'updates all of foo',
- * 'delete' => 'empty up foo'),
- * '/bar' => array(...),
- * ));
- * \endcode
- *
- * @param String|Array $uri_template URI template to describe or pass an
- * array to describe multiple routes.
- * @param String|null $description description of the route
- * @param String $method method to describe.
- *
- * @return Router returns instance of itself to allow chaining
- */
- public function describe($uri_template, $description = null, $method = 'get')
- {
- // describe multiple routes at once
- if (func_num_args() === 1 && is_array($uri_template)) {
- foreach ($uri_template as $template => $description) {
- $this->describe($template, $description);
- }
- }
-
- // describe routes that use the same URI template
- elseif (func_num_args() === 2 && is_array($description)) {
- foreach ($description as $method => $desc) {
- $this->describe($uri_template, $desc, $method);
- }
- }
-
- // describe a single route
- else {
- if (!isset($this->descriptions[$uri_template])) {
- $this->descriptions[$uri_template] = [];
- }
- if (isset($this->routes[$method][$uri_template])) {
- $this->descriptions[$uri_template][$method] = $description;
- } else {
- // Try to find route with different method
- foreach ($this->routes as $m => $templates) {
- if (isset($templates[$uri_template])) {
- $this->descriptions[$uri_template][$m] = $description;
- break;
- }
- }
- }
- }
- return $this;
- }
-
- /**
- * Get list of registered routes - optionally with their descriptions.
- *
- * @param bool $describe (optional) include descriptions,
- * defaults to `false`
- * @param bool $check_access (optional) only show methods this router's
- * consumer is authorized to,
- * defaults to `true`
- *
- * @return array list of registered routes
- */
- public function getRoutes($describe = false, $check_access = true)
- {
- $this->setupRoutes();
-
- $result = [];
- foreach ($this->routes as $method => $routes) {
- foreach ($routes as $uri => $route) {
- if ($check_access && !$this->permissions->check($uri, $method)) {
- continue;
- }
- if (!isset($result[$uri])) {
- $result[$uri] = [];
- }
- if ($describe) {
- $result[$uri][$method] = [
- 'description' => $this->descriptions[$uri][$method] ?? null,
- 'source' => $route['source'] ?? 'unknown',
- ];
- } else {
- $result[$uri][] = $method;
- }
- }
- }
- ksort($result);
- if ($describe) {
- $result = array_map(function ($item) {
- ksort($item);
- return $item;
- }, $result);
- }
- return $result;
- }
-
- /**
- * Dispatches an URI across the defined routes and produces a
- * Response object which may then be send back (using #output).
- *
- * @param mixed $uri URI to dispatch (defaults to `$_SERVER['PATH_INFO']`)
- * @param String $method Request method (defaults to the method
- * of the actual HTTP request or "GET")
- *
- * @return Response a Response object containing status, headers
- * and body
- * @throws RouterException may throw such an exception if there
- * is no matching route (404) or if there
- * is one, but the consumer is not
- * authorized to it (403)
- */
- public function dispatch($uri = null, $method = null)
- {
- $this->setupRoutes();
-
- $uri = $this->normalizeDispatchURI($uri);
- $method = $this->normalizeRequestMethod($method);
-
- $content_renderer = $this->negotiateContent($uri);
-
- $match_result = $this->matchRoute($uri, $method, $content_renderer);
- $route = $match_result[0];
- $parameters = $match_result[1];
- $allow_nobody = $match_result[2] ?? false;
- if (!$route) {
- //No route found for the combination of URI and method.
- //We return the allowed methods for the route in the HTTP header:
- $methods = $this->getMethodsForUri($uri);
- if (count($methods) > 0) {
- header('Allow: ' . implode(', ', $methods));
- throw new RouterException(405);
- } else {
- //Route not found.
- throw new RouterException(404);
- }
- }
- //At this point, a route is found.
- //We need to check if it can be used as nobody user or not.
- if (!$route['allow_nobody'] && !$this->user) {
- //Nobody users aren't allowed for this route.
- throw new RouterException(401, 'Unauthorized (no consumer)');
- }
-
- try {
- $response = $this->execute($route, $parameters);
- } catch (RouterHalt $halt) {
- $response = $halt->response;
- }
-
- $response->finish($content_renderer);
-
- return $response;
- }
-
- /**
- * Searches and registers available routes.
- */
- private function setupRoutes()
- {
- // A bit ugly, I confess
- static $was_setup = false;
- if ($was_setup) {
- return;
- }
- $was_setup = true;
-
- // Register default routes
- $routes = [
- 'Activity',
- 'Blubber',
- 'Clipboard',
- 'Contacts',
- 'Course',
- 'Discovery',
- 'Events',
- 'Feedback',
- 'FileSystem',
- 'Forum',
- 'Messages',
- 'News',
- 'ResourceBooking',
- 'Resources',
- 'ResourceCategories',
- 'ResourcePermissions',
- 'ResourceProperties',
- 'ResourceRequest',
- 'RoomClipboard',
- 'Schedule',
- 'Semester',
- 'Studip',
- 'User',
- 'UserConfig',
- 'Wiki'
- ];
-
- foreach ($routes as $route) {
- require_once "app/routes/$route.php";
- $class = "\\RESTAPI\\Routes\\$route";
- $this->registerRoutes(new $class);
- }
-
- // Register plugin routes
- $router = $this;
- $routes = array_flatten(\PluginEngine::sendMessage('RESTAPIPlugin', 'getRouteMaps'));
- array_walk(
- $routes,
- function ($route) use ($router) {
- $router->registerRoutes($route);
- }
- );
- }
-
- /**
- * Takes a route and the parameters out of the requested path and
- * executes the handler of the route.
- *
- * @param array $route the matched route out of
- * Router::matchRoute; an array with keys
- * 'handler', 'conditions' and 'source'
- * @param array $parameters the matched parameters out of
- * Router::matchRoute; something like:
- * `array('user_id' => '23a21d...e78f')`
- * @return Response the resulting Response object which is then
- * polished in Router::dispatch
- */
- protected function execute($route, $parameters)
- {
- $handler = $route['handler'];
-
- if (!is_object($handler[0])) {
- throw new \RuntimeException("Handler is not a method.");
- }
-
- $handler[0]->init($this, $route);
-
- if (method_exists($handler[0], 'before')) {
- $handler[0]->before($this, $handler, $parameters);
- }
-
- $result = call_user_func_array($handler, $parameters);
-
- if (is_object($result) && method_exists($result, 'toArray')) {
- $result = $result->toArray();
- }
-
- // $result is stronger than $response->body
- if (isset($result)) {
- $handler[0]->body($result);
- }
-
- if (method_exists($handler[0], 'after')) {
- $handler[0]->after($this, $parameters);
- }
-
- return $handler[0]->getResponse();
- }
-
- /**
- * Registers a content renderer.
- *
- * @param DefaultRenderer $renderer instance of a content renderer
- * @param boolean $is_default (optional) set this
- * renderer as default?;
- * defaults to `false`
- *
- * @return Router returns itself to allow chaining
- */
- public function registerRenderer($renderer, $is_default = false)
- {
- $this->renderers[$renderer->extension()] = $renderer;
- if ($is_default) {
- $this->default_renderer = $renderer;
- }
-
- return $this;
- }
-
- private function normalizeDispatchURI($uri)
- {
- return $uri ?? \Request::pathInfo();
- }
-
- private function normalizeRequestMethod($method)
- {
- return mb_strtolower($method ?: \Request::method() ?: 'get');
- }
-
- /**
- * Negotiate content using the registered content renderers. The
- * first ContentRenderer that returns `true` when calling
- * ContentRenderer::shouldRespondTo gets the job.
- *
- * @param String $uri the URI to which the content renderers may respond
- *
- * @return ContentRenderer either a ContentRenderer that responds
- * to the URI or the default
- * ContentRenderer of this router.
- */
- protected function negotiateContent($uri)
- {
- $content_renderer = null;
- foreach ($this->renderers as $renderer) {
- if ($renderer->shouldRespondTo($uri)) {
- $content_renderer = $renderer;
- break;
- }
- }
- if (!$content_renderer) {
- $content_renderer = $this->default_renderer ?: reset($this->renderers);
- }
- return $content_renderer;
- }
-
- /**
- * Tries to match a route given a URI and a HTTP request method.
- *
- * @param String $uri the URI to match
- * @param String $method the HTTP request method to match
- * @param DefaultRenderer $content_renderer the used
- * ContentRenderer which
- * is needed to remove
- * a file extension
- *
- * @return array an array containing the matched route and the
- * found parameters
- */
- protected function matchRoute($uri, $method, $content_renderer)
- {
- $matched = null;
- $parameters = [];
- if (isset($this->routes[$method])) {
- if ($content_renderer->extension() && mb_strpos($uri, $content_renderer->extension()) !== false) {
- $uri = mb_substr($uri, 0, -mb_strlen($content_renderer->extension()));
- }
-
- foreach ($this->routes[$method] as $uri_template => $route) {
- if (!isset($route['uri_template'])) {
- $route['uri_template'] = new UriTemplate($uri_template, $route['conditions']);
- }
-
- $prmtrs = null; // Will be filled by a successful match()
- if ($route['uri_template']->match($uri, $prmtrs)) {
- if (!$this->permissions->check($uri_template, $method)) {
- throw new RouterException(403, "Route not activated");
- }
- $matched = $route;
- $parameters = $prmtrs;
- break;
- }
- }
- }
- return [$matched, $parameters];
- }
-
- /**
- * Returns all methods the given uri responds to.
- *
- * @param String $uri the URI to match
- *
- * @return array of all of responding methods
- */
- protected function getMethodsForUri($uri)
- {
- $methods = [];
-
- foreach ($this->routes as $method => $templates) {
- foreach ($templates as $uri_template => $route) {
- if (!isset($route['uri_template'])) {
- $route['uri_template'] = new UriTemplate($uri_template, $route['conditions']);
- }
-
- if ($route['uri_template']->match($uri)
- && $this->permissions->check($uri_template, $method))
- {
- $methods[] = $method;
- }
- }
- }
-
- return array_map('strtoupper', $methods);
- }
-
-
- /**
- * Sets up the authentication for the router.
- */
- public function setupAuth()
- {
- // Detect consumer
- $consumer = Consumer\Base::detectConsumer();
- if (!$consumer) {
- return null;
- }
-
- $this->user = $consumer->getUser();
-
- // Set authentication if present
- if ($this->user) {
- // Skip fake authentication if user is already logged in
- if ($GLOBALS['user']->id !== $this->user->id) {
-
- $GLOBALS['auth'] = new \Seminar_Auth();
- $GLOBALS['auth']->auth = [
- 'uid' => $this->user->user_id,
- 'uname' => $this->user->username,
- 'perm' => $this->user->perms,
- ];
-
- $GLOBALS['user'] = new \Seminar_User($this->user);
-
- $GLOBALS['perm'] = new \Seminar_Perm();
- $GLOBALS['MAIL_VALIDATE_BOX'] = false;
- }
- setTempLanguage($GLOBALS['user']->id);
- }
-
- return $this->user;
- }
-}
diff --git a/lib/classes/restapi/RouterException.php b/lib/classes/restapi/RouterException.php
deleted file mode 100644
index 1ce2afc..0000000
--- a/lib/classes/restapi/RouterException.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-namespace RESTAPI;
-use \Exception;
-
-/**
- * Router exception.
- *
- * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
- * @author <mlunzena@uos.de>
- * @license GPL 2 or later
- * @since Stud.IP 3.0
- * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0.
- */
-class RouterException extends Exception
-{
- protected static $error_messages = [
- 400 => 'Bad Request',
- 401 => 'Unauthorized',
- 403 => 'Forbidden',
- 404 => 'Not Found',
- 405 => 'Method Not Allowed',
- 500 => 'Internal Server Error',
- 501 => 'Not implemented',
- ];
-
- public function __construct($code = 500, $message = '', $previous = null)
- {
- $message = $message ?: self::$error_messages[$code] ?: '';
- parent::__construct($message, $code, $previous);
- }
-}
diff --git a/lib/classes/restapi/RouterHalt.php b/lib/classes/restapi/RouterHalt.php
deleted file mode 100644
index 55a2ca1..0000000
--- a/lib/classes/restapi/RouterHalt.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-namespace RESTAPI;
-
-/**
- * @author <mlunzena@uos.de>
- * @license GPL 2 or later
- * @since Stud.IP 3.0
- * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0.
- */
-class RouterHalt extends \Exception
-{
- public $response;
-
- public function __construct($response)
- {
- parent::__construct();
- $this->response = $response;
- }
-}
diff --git a/lib/classes/restapi/UriTemplate.php b/lib/classes/restapi/UriTemplate.php
deleted file mode 100644
index 67161de..0000000
--- a/lib/classes/restapi/UriTemplate.php
+++ /dev/null
@@ -1,115 +0,0 @@
-<?php
-namespace RESTAPI;
-
-/**
- * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
- * @author <mlunzena@uos.de>
- * @license GPL 2 or later
- * @since Stud.IP 3.0
- * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0.
- */
-class UriTemplate
-{
- public $uri_template;
- public $conditions;
-
- public function __construct($uri_template, $conditions = [])
- {
- $this->uri_template = $uri_template;
- $this->conditions = $conditions;
- }
-
- /**
- * Tests whether an uri matches a template.
- *
- * The template may contain placeholders by prefixing an appropriate,
- * unique placeholder name with a colon (:).
- *
- * <code>$template = '/hello/:name';</code>
- *
- * If the uri matches the template, all evaluated placeholders will
- * be stored in the parameters array.
- *
- * @param String $uri The uri to test
- * @param array $parameters Stores evaluated parameters on match (optional)
- *
- * @return bool Returns true if the uri matches the template
- */
- public function match($uri, &$parameters = null)
- {
- // Initialize parameters array
- $parameters = [];
-
- // Split and normalize uri and template
- $given = array_filter(explode('/', $uri), 'mb_strlen');
- $rules = array_filter(explode('/', $this->uri_template));
-
- // Leave if uri and template do not contain the same number of
- // elements
- if (count($given) !== count($rules)) {
- return false;
- }
-
- // Combine uri and template element-wise (simplifies iteration)
- $combined = array_combine($rules, $given);
-
- // Iterate over uri and template and compare element by element
- foreach ($combined as $rule => $actual) {
- if ($rule[0] === ':') {
- // Rule is a placeholder
- $parameter_name = mb_substr($rule, 1);
-
- if (isset($this->conditions[$parameter_name])
- && !preg_match($this->conditions[$parameter_name], $actual)) {
- return false;
- }
-
- $parameters[$parameter_name] = $actual;
-
- } elseif ($actual !== $rule) {
- // Elements do not match
- $parameters = [];
- return false;
- }
- }
-
- return true;
- }
-
-
- public function inject($params)
- {
- // Initialize parameters array
- $parameters = [];
-
- // Split and normalize template
- $rules = array_filter(explode('/', $this->uri_template));
-
- foreach ($rules as &$rule) {
-
- // Rule is a placeholder
- if ($rule[0] === ':') {
- $parameter_name = mb_substr($rule, 1);
-
- if (!isset($params[$parameter_name])) {
- $reason = sprintf('UriTemplate parameter :%s missing.',
- htmlReady($parameter_name));
- throw new \RuntimeException($reason);
- }
-
- $actual = $params[$parameter_name];
-
- if (isset($this->conditions[$parameter_name])
- && !preg_match($this->conditions[$parameter_name], $actual)) {
- $reason = sprintf('UriTemplate parameter :%s did not satisfy its condition.',
- htmlReady($parameter_name));
- throw new \RuntimeException($reason);
- }
-
- $rule = htmlReady($actual);
- }
- }
-
- return join('/', $rules);
- }
-}
diff --git a/lib/classes/restapi/UserPermissions.php b/lib/classes/restapi/UserPermissions.php
deleted file mode 100644
index dcf1601..0000000
--- a/lib/classes/restapi/UserPermissions.php
+++ /dev/null
@@ -1,144 +0,0 @@
-<?php
-namespace RESTAPI;
-use DBManager, PDO;
-
-/**
- * REST API routing permissions
- *
- * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
- * @license GPL 2 or later
- * @since Stud.IP 2.6
- * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0.
- */
-class UserPermissions
-{
- /**
- * Create a permission object (for a certain user).
- * Permissions object will be cached for each user.
- *
- * @param mixed $user_id Id of user (optional, defaults to global)
- * @return UserPermissions Returns permissions object
- */
- public static function get($user_id = null)
- {
- $user_id = $user_id ?: $GLOBALS['user']->id;
-
- static $cache = [];
- if (!isset($cache[$user_id])) {
- $cache[$user_id] = new self($user_id);
- }
-
- return $cache[$user_id];
- }
-
- private $user_id;
- private $permissions = [];
-
- /**
- * Creates the actual permission object (for a certain user).
- *
- * @param mixed $user_id Id of user (optional, defaults to global)
- */
- private function __construct($user_id = null)
- {
- $this->user_id = $user_id;
-
- // Init with global permissions
- $this->loadPermissions();
- }
-
- /**
- * Defines whether access is allowed for the current user to the
- * passed route via the passed method.
- *
- * @param String $user_id Id of the user
- * @param mixed $granted Granted state (PHP'ish boolean)
- * @return UserPermissions Returns instance of self to allow chaining
- */
- public function set($user_id, $granted = true)
- {
- $this->permissions[$user_id] = (bool)$granted;
-
- return $this;
- }
-
- /**
- * Loads permissions for passed user.
- *
- * @return UserPermissions Returns instance of self to allow chaining
- */
- protected function loadPermissions()
- {
- $query = "SELECT consumer_id, granted
- FROM api_user_permissions
- WHERE user_id = :user_id";
- $statement = DBManager::get()->prepare($query);
- $statement->bindValue(':user_id', $this->user_id);
- $statement->execute();
- $permissions = $statement->fetchAll(PDO::FETCH_ASSOC);
-
- // Init with global permissions
- foreach ($permissions as $permission) {
- extract($permission);
-
- $this->set($permission['consumer_id'], $permission['granted']);
- }
-
- return $this;
- }
-
- /**
- * Checks if access to consumer is allowed for the current user.
- *
- * @param String $consumer_id Id of the consumer
- * @return bool Indicates whether access is allowed
- */
- public function check($consumer_id)
- {
- return isset($this->permissions[$consumer_id])
- && $this->permissions[$consumer_id];
- }
-
- /**
- * Stores the set permissions.
- *
- * @return bool Returns true if permissions were stored successfully
- */
- public function store()
- {
- $result = true;
-
- $query = "INSERT INTO api_user_permissions (user_id, consumer_id, granted, mkdate, chdate)
- VALUES (:user_id, :consumer_id, :granted, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())
- ON DUPLICATE KEY UPDATE granted = VALUES(granted),
- chdate = UNIX_TIMESTAMP()";
- $statement = DBManager::get()->prepare($query);
- $statement->bindValue(':user_id', $this->user_id);
-
- foreach ($this->permissions as $consumer_id => $granted) {
- $statement->bindValue(':consumer_id', $consumer_id);
- $statement->bindValue(':granted', (int) !empty($granted));
-
- $result = $result && $statement->execute();
- }
-
- return $result;
- }
-
- /**
- * Get a list of all consumer the user has granted acces to.
- *
- * @return array List of consumer objects
- */
- public function getConsumers()
- {
- $result = [];
- foreach ($this->permissions as $consumer_id => $granted) {
- if (!$granted) {
- continue;
- }
- $result[$consumer_id] = Consumer\Base::find($consumer_id);
- }
- return $result;
- }
-}
diff --git a/lib/classes/restapi/consumer/Base.php b/lib/classes/restapi/consumer/Base.php
deleted file mode 100644
index 50f3150..0000000
--- a/lib/classes/restapi/consumer/Base.php
+++ /dev/null
@@ -1,226 +0,0 @@
-<?php
-namespace RESTAPI\Consumer;
-
-use AuthUserMd5;
-use DBManager;
-use DBManagerException;
-use PDO;
-
-/**
- * Base consumer class for the rest api
- *
- * Consumers provide means for authenticating a user and the access
- * permissions for routes are bound to specific consumers.
- *
- * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
- * @license GPL 2 or later
- * @since Stud.IP 3.0
- * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0.
- */
-abstract class Base extends \SimpleORMap
-{
- /**
- * Each consumer type has to implement a detect feature which
- * should extract crucial information from the request and return
- * an instance of itself if the consumer detects a valid signature
- * it can respond to.
- *
- * @param mixed $request_type Type of request (optional; defaults to any)
- * @return mixed Detected consumer object or false
- */
- abstract public static function detect($request_type = null);
-
- /* Concrete */
-
- /**
- * Configures the model.
- *
- * @param array $config Configuration array
- */
- protected static function configure($config = [])
- {
- $config['db_table'] = 'api_consumers';
-
- parent::configure($config);
- }
-
- /**
- * Stores all known consumer types
- */
- protected static $known_types = [];
-
- /**
- * Add a consumer type to the list of consumer types
- *
- * @param String $type Name of the type
- * @param String $class Associated consumer class
- */
- public static function addType($type, $class)
- {
- self::$known_types[$type] = $class;
- }
-
- /**
- * Removes a consumer type from the list of consumer types
- *
- * @param String $type Name of the type
- */
- public static function removeType($type)
- {
- unset(self::$known_types[$type]);
- }
-
- /**
- * Overloaded find method. Will return a concrete specialized consumer
- * object of the associated type.
- *
- * @param String $id Id of the consumer
- * @return \RESTAPI\Consumer\Base Associated consumer object (derived
- * from consumer base type)
- * @throws \Exception if either consumer id or consumer type is invalid
- */
- public static function find($id)
- {
- $query = "SELECT consumer_type
- FROM api_consumers
- WHERE consumer_id = :id";
- $statement = DBManager::get()->prepare($query);
- $statement->bindValue(':id', $id);
- $statement->execute();
- $type = $statement->fetchColumn();
-
- if (!isset(self::$known_types[$type])) {
- throw new \Exception('Consumer #' . $id . ' is of unknown type "' . $type . '"');
- }
-
- return new self::$known_types[$type]($id);
- }
-
- /**
- * Returns a list of all known consumers.
- *
- * @return array List of all known consumers (as specialized consumer
- * objects)
- */
- public static function findAll()
- {
- $query = "SELECT consumer_id FROM api_consumers";
- $statement = DBManager::get()->query($query);
- $ids = $statement->fetchAll(PDO::FETCH_COLUMN);
-
- return array_map([self::class, 'find'], $ids);
- }
-
- /**
- * Creates a new consumer of the given type.
- *
- * @param String $type Name of the type
- * @return \RESTAPI\Consumer\Base Consumer object of the given (derived
- * from consumer base type)
- * @throws \Exception if type is invalid
- */
- public static function create($type)
- {
- if (!isset(self::$known_types[$type])) {
- throw new \Exception('Consumer is of unknown type "' . $type . '"');
- }
-
- return new self::$known_types[$type];
- }
-
- /**
- * This method is used to detect a consumer (of a specific type) by
- * executing the detect method on all known consumer types.
- *
- * @param mixed $type Name of the type (optional; defaults to all types)
- * @param mixed $request_type Type of request (optional; defaults to any)
- * @return mixed Either the detected consumer or false if no consumer
- * was detected
- * @throws \Exception if type is invalid
- */
- public static function detectConsumer($type = null, $request_type = null)
- {
- $needles = $type === null
- ? array_keys(self::$known_types)
- : [$type];
- foreach ($needles as $needle) {
- if (!isset(self::$known_types)) {
- throw new \Exception('Trying to detect consumer of unkown type "' . $needle . '"');
- }
- $consumer_class = self::$known_types[$needle];
- if ($consumer = $consumer_class::detect($request_type)) {
- return $consumer;
- }
- }
- return false;
- }
-
- /**
- * Contains user information
- */
- protected $user = null;
-
- /**
- * Extended SimpleORMap constructor. A certain user can be injected upon
- * creation.
- *
- * @param mixed $id Id of the consumer or null to create a new one
- * @param mixed $user Either a user object or id to inject to the consumer
- * or null if no user should be injected
- */
- public function __construct($id = null, $user = null)
- {
- parent::__construct($id);
-
- if ($user !== null) {
- $this->setUser($user);
- }
- }
-
- /**
- * Retrieve the api permissions associated with this consumer.
- *
- * @return \RESTAPI\ConsumerPermissions Permission object for this consumer
- */
- public function getPermissions()
- {
- return \RESTAPI\ConsumerPermissions::get($this->id);
- }
-
- /**
- * Inject a user to this consumer. Injecting in this context refers to
- * "having a user authenticated by this consumer".
- *
- * @param mixed $user Either a user object or a user id
- * @return \RESTAPI\Consumer\Base Returns instance of self to allow
- * chaining
- */
- public function setUser($user)
- {
- if (!is_object($user)) {
- $user = \User::findFull($user);
- }
- $this->user = $user;
- return $this;
- }
-
- /**
- * Returns whether the consumer has an injected user or not.
- *
- * @return bool True if a valid user is found, false otherwise
- */
- public function hasUser()
- {
- return $this->user !== null && $this->user->id && $this->user->id !== 'nobody';
- }
-
- /**
- * Return the injected user.
- *
- * @param mixed User object or false if no user was injected
- */
- public function getUser()
- {
- return $this->user;
- }
-}
diff --git a/lib/classes/restapi/consumer/HTTP.php b/lib/classes/restapi/consumer/HTTP.php
deleted file mode 100644
index 97b0657..0000000
--- a/lib/classes/restapi/consumer/HTTP.php
+++ /dev/null
@@ -1,50 +0,0 @@
-<?php
-namespace RESTAPI\Consumer;
-use StudipAuthAbstract, RESTAPI\RouterException;
-
-/**
- * Basic HTTP Authentication consumer for the rest api
- *
- * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
- * @license GPL 2 or later
- * @since Stud.IP 3.0
- * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0.
- */
-class HTTP extends Base
-{
- /**
- * Detects if a user is authenticated via basic http authentication.
- * The only supported authentication for now is via the url:
- *
- * http://username:password@host/path?query
- *
- * @param mixed $request_type Type of request (optional; defaults to any)
- * @return mixed Instance of self if authentication was detected, false
- * otherwise
- * @throws RouterException if authentication fails
- * @todo Integrate and test HTTP_AUTHORIZATION header authentication
- */
- public static function detect($request_type = null)
- {
- if (
- isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])
- || isset($_SERVER['HTTP_AUTHORIZATION'])
- ) {
- $user_id = false;
-
- if (isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
- $username = $_SERVER['PHP_AUTH_USER'];
- $password = $_SERVER['PHP_AUTH_PW'];
- } elseif (isset($_SERVER['HTTP_AUTHORIZATION'])) {
- list($username, $password) = explode(':', base64_decode(mb_substr($_SERVER['HTTP_AUTHORIZATION'], 6)));
- }
-
- $check = StudipAuthAbstract::CheckAuthentication($username, $password);
- if ($check['uid'] && $check['uid'] !== 'nobody') {
- return new self(null, $check['uid']);
- }
-
- }
- return false;
- }
-}
diff --git a/lib/classes/restapi/consumer/OAuth.php b/lib/classes/restapi/consumer/OAuth.php
deleted file mode 100644
index caf51c2..0000000
--- a/lib/classes/restapi/consumer/OAuth.php
+++ /dev/null
@@ -1,231 +0,0 @@
-<?php
-namespace RESTAPI\Consumer;
-use StudipAutoloader, DBManager, OAuthRequestVerifier, OAuthStore, OAuthServer, Exception;
-use \RESTAPI\UserPermissions;
-
-StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'] . DIRECTORY_SEPARATOR . 'vendor/oauth-php/library/');
-
-/**
- * OAuth consumer for the rest api
- *
- * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
- * @license GPL 2 or later
- * @since Stud.IP 3.0
- * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0.
- */
-class OAuth extends Base
-{
- /**
- * Configures the model.
- *
- * @param array $config Configuration array
- */
- protected static function configure($config = [])
- {
- $config['default_values']['consumer_type'] = 'oauth';
-
- $config['registered_callbacks']['before_store'][] = 'before_store';
-
- parent::configure($config);
- }
-
- /**
- * Detects whether the request is authenticated via OAuth.
- *
- * @param mixed $request_type Type of request (optional; defaults to any)
- * @return mixed Instance of self if authentication was detected, false
- * otherwise
- */
- public static function detect($request_type = null)
- {
- if (OAuthRequestVerifier::requestIsSigned() && $request_type !== 'request') {
- $user_id = false;
-
- $parameters = (in_array($_SERVER['REQUEST_METHOD'], ['GET', 'POST']))
- ? null
- : $GLOBALS['_' . $_SERVER['REQUEST_METHOD']];
-
- $req = new OAuthRequestVerifier(null, null, $parameters);
-
- // Check oauth timestamp and deny access if timestamp is outdated
- if ($req->getParam('oauth_timestamp') < strtotime('-6 hours')) {
- return false;
- }
- $result = $req->verifyExtended('access');
-
- // @todo
- # self::$consumer_key = $result['consumer_key'];
-
- $query = "SELECT user_id FROM api_oauth_user_mapping WHERE oauth_id = :oauth_id";
- $statement = DBManager::get()->prepare($query);
- $statement->bindValue(':oauth_id', $result['user_id']);
- $statement->execute();
- $user_id = $statement->fetchColumn();
-
- if (!$user_id) {
- return false;
- }
-
- $consumer = reset(self::findByAuth_Key($result['consumer_key']));
- $consumer->setUser($user_id);
- return $consumer;
- } else {
- try {
- // Check if there is a valid request token in the current request
- // Returns an array with the consumer key, consumer secret, token, token secret and token type.
- $rs = self::getServer()->authorizeVerify();
-
- $query = "SELECT consumer_id
- FROM api_consumers
- WHERE auth_key = :key";
- $statement = DBManager::get()->prepare($query);
- $statement->bindValue(':key', $rs['consumer_key']);
- $statement->execute();
- $id = $statement->fetchColumn();
-
- if ($id) {
- return new self($id);
- }
- } catch (Exception $e) {
- }
- }
- return false;
- }
-
- /**
- * Returns a singleton instance of the oauth server.
- *
- * @return OAuthServer The server object
- */
- public static function getServer()
- {
- static $server = null;
- if ($server === null) {
- $server = new OAuthServer(null, null, null, 'SESSION', [], [
- 'allowed_uri_schemes' => []
- ]);
- }
- return $server;
- }
-
- /**
- * "Before store" trigger. Creates a clone of the consumer in the
- * tables for the vendor oauth library.
- */
- protected function before_store()
- {
- static $mapping = [
- 'auth_key' => 'consumer_key',
- 'auth_secret' => 'consumer_secret',
- 'active' => 'enabled',
- 'contact' => 'requester_name',
- 'email' => 'requester_email',
- 'callback' => 'callback_uri',
- 'url' => 'application_uri',
- 'title' => 'application_title',
- 'description' => 'application_descr',
- 'notes' => 'application_notes',
- 'type' => 'application_type',
- 'commercial' => 'application_commercial',
- ];
-
- $consumer = [];
- foreach ($mapping as $from => $to) {
- $consumer[$to] = $this->$from;
- }
-
- $query = "SELECT osr_id
- FROM oauth_server_registry
- WHERE osr_consumer_key = :key AND osr_consumer_secret = :secret";
- $statement = DBManager::get()->prepare($query);
- $statement->bindValue(':key', $this->auth_key);
- $statement->bindValue(':secret', $this->auth_secret);
- $statement->execute();
- $consumer['id'] = $statement->fetchColumn();
-
- $consumer_key = OAuthStore::instance('PDO')->updateConsumer($consumer, null, true);
-
- if ($this->isNew()) {
- $consumer = OAuthStore::instance('PDO')->getConsumer($consumer_key, null, true);
- $this->auth_key = $consumer['consumer_key'];
- $this->auth_secret = $consumer['consumer_secret'];
- }
- }
-
- /**
- * Grant oauth access for a user.
- *
- * @param mixed $user_id Specific user id or null to default to the
- * injected user
- * @throws Exception If no valid user is present
- */
- public function grantAccess($user_id = null)
- {
- if ($user_id === null && $this->hasUser()) {
- $user_id = $this->user->id;
- }
- if (!$user_id) {
- throw new Exception('Can not grant access to unknown user');
- }
-
- UserPermissions::get($GLOBALS['user']->id)->set($this->id, true)->store();
- return self::getServer()->authorizeFinish(true, self::getOAuthId($user_id));
- }
-
- /**
- * Revoke oauth access from a user.
- *
- * @param mixed $user_id Specific user id or null to default to the
- * injected user
- * @throws Exception If no valid user is present
- */
- public function revokeAccess($user_id = null)
- {
- if ($user_id === null && $this->hasUser()) {
- $user_id = $this->user->id;
- }
- if (!$user_id) {
- throw new Exception('Can not revoke access from unknown user');
- }
-
- $query = "DELETE oauth_server_token
- FROM oauth_server_token
- JOIN oauth_server_registry
- WHERE ost_usa_id_ref = :id AND osr_consumer_key = :key AND osr_consumer_secret = :secret";
- $statement = DBManager::get()->prepare($query);
- $statement->bindValue(':id', self::getOAuthId($user_id));
- $statement->bindValue(':key', $this->auth_key);
- $statement->bindValue(':secret', $this->auth_secret);
- $statement->execute();
-
- UserPermissions::get($GLOBALS['user']->id)->set($this->id, false)->store();
- return self::getServer()->authorizeFinish(false, self::getOAuthId($user_id));
- }
-
- /**
- * Maps a user to an oauth id. This is neccessary due to the fact that
- * the oauth lib works with different ids than Stud.IP.
- *
- * @param String $user_id Id of the user to get an oauth id for
- * @return String The mapped oauth id
- */
- public static function getOAuthId($user_id)
- {
- $query = "SELECT oauth_id FROM api_oauth_user_mapping WHERE user_id = :id";
- $statement = DBManager::get()->prepare($query);
- $statement->bindValue(':id', $user_id);
- $statement->execute();
- $oauth_id = $statement->fetchColumn();
-
- if (!$oauth_id) {
- $query = "INSERT INTO api_oauth_user_mapping (user_id, mkdate)
- VALUES (:id, UNIX_TIMESTAMP())";
- $statement = DBManager::get()->prepare($query);
- $statement->bindValue(':id', $user_id);
- $statement->execute();
- $oauth_id = DBManager::get()->lastInsertId();
- }
-
- return $oauth_id;
- }
-}
diff --git a/lib/classes/restapi/consumer/Studip.php b/lib/classes/restapi/consumer/Studip.php
deleted file mode 100644
index 738dd75..0000000
--- a/lib/classes/restapi/consumer/Studip.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-namespace RESTAPI\Consumer;
-
-/**
- * Stud.IP Session Consumer for the rest api
- *
- * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
- * @license GPL 2 or later
- * @since Stud.IP 3.0
- * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0.
- */
-class Studip extends Base
-{
- /**
- * Detects a user via the Stud.IP session. If a session is present and
- * valid, the auth and user object have already been set up by stud.ip
- * functions, so we just need to check if these are present.
- *
- * @param mixed $request_type Type of request (optional; defaults to any)
- * @return mixed Instance of self if authentication was detected, false
- * otherwise
- */
- public static function detect($request_type = null)
- {
- if (
- !isset($GLOBALS['auth'])
- || !$GLOBALS['auth']->is_authenticated()
- || $GLOBALS['user']->id === 'nobody'
- || !\CSRFProtection::verifyRequest()
- ) {
- return false;
- }
-
- return new self(null, $GLOBALS['user']->id);
- }
-}
diff --git a/lib/classes/restapi/renderer/DebugRenderer.php b/lib/classes/restapi/renderer/DebugRenderer.php
deleted file mode 100644
index afd56f6..0000000
--- a/lib/classes/restapi/renderer/DebugRenderer.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-namespace RESTAPI\Renderer;
-
-/**
- * Debug content renderer.
- *
- * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
- * @author <mlunzena@uos.de>
- * @license GPL 2 or later
- * @since Stud.IP 3.0
- * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0.
- */
-class DebugRenderer extends DefaultRenderer
-{
- /**
- * Returns an associated content type.
- */
- public function contentType()
- {
- return 'text/plain';
- }
-
- /**
- * Returns an associated extension.
- */
- public function extension()
- {
- return '.debug';
- }
-
- /**
- * Response transformation function.
- *
- * @param \RESTAPI\Response $response the response to transform
- */
- public function render($response)
- {
- if (!isset($response['Content-Type'])) {
- $response['Content-Type'] = $this->contentType() . ';charset=utf-8';
- }
-
- $debug = function ($label, $data) {
- echo str_pad('', 78, '=') . PHP_EOL;
- echo str_pad('- ' . $label, 77, ' ') . '-' . PHP_EOL;
- echo str_pad('', 78, '=') . PHP_EOL;
- var_export($data);
- echo PHP_EOL;
- };
-
- ob_start();
- $debug('Response Status', $response->status);
- $debug('Response Header', $response->headers);
- $debug('Response Body', $response->body);
- $debug('Request', $GLOBALS['_' . $_SERVER['REQUEST_METHOD']]);
- $response->body = ob_get_clean();
- }
-}
diff --git a/lib/classes/restapi/renderer/DefaultRenderer.php b/lib/classes/restapi/renderer/DefaultRenderer.php
deleted file mode 100644
index 836ba36..0000000
--- a/lib/classes/restapi/renderer/DefaultRenderer.php
+++ /dev/null
@@ -1,74 +0,0 @@
-<?php
-namespace RESTAPI\Renderer;
-
-/**
- * Default base content renderer class (outputs text/plain).
- *
- * Content renderers are output filters that can reshape data before it
- * is sent to the client.
- * Each content renderer is associated with a certain content type and a
- * certain file extension. This is neccessary for content negotiation.
- *
- * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
- * @author <mlunzena@uos.de>
- * @license GPL 2 or later
- * @since Stud.IP 3.0
- * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0.
- */
-class DefaultRenderer
-{
- /**
- * Returns an associated content type.
- *
- * @return String Content/mime type for this renderer
- */
- public function contentType()
- {
- return 'text/plain';
- }
-
- /**
- * Returns an associated extension.
- *
- * @return String Associated extension for this renderer.
- */
- public function extension()
- {
- return '';
- }
-
- /**
- * Response transformation function.
- *
- * @param \RESTAPI\Response $response the response to transform
- */
- public function render($response)
- {
- if (!isset($response['Content-Type'])) {
- $response['Content-Type'] = $this->contentType() . ';charset=utf-8';
- }
- }
-
- /**
- * Detects whether the renderer should respond to either a certain
- * filename (tests by extension) or to a certain media range.
- *
- * @param String $filename Filename to test against
- * @param mixed $media_range Media range to test against (optional,
- * defaults to request's accept header if set)
- * @return bool Returns whether the renderer should respond
- */
- public function shouldRespondTo($filename, $media_range = null)
- {
- // If no media range is passed, evalute http header "Accept"
- if ($media_range === null && isset($_SERVER['ACCEPT'])) {
- $media_ranges = explode(';', $_SERVER['ACCEPT']);
- $media_range = reset($media_ranges);
- }
-
- // Test if either the filename has the appropriate extension or
- // if the client accepts the content type
- return ($this->extension() && fnmatch('*' . $this->extension(), $filename))
- || ($media_range && fnmatch($media_range, $this->contentType()));
- }
-}
diff --git a/lib/classes/restapi/renderer/JSONRenderer.php b/lib/classes/restapi/renderer/JSONRenderer.php
deleted file mode 100644
index 9c6e449..0000000
--- a/lib/classes/restapi/renderer/JSONRenderer.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-namespace RESTAPI\Renderer;
-
-/**
- * Content renderer for json content.
- *
- * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
- * @author <mlunzena@uos.de>
- * @license GPL 2 or later
- * @since Stud.IP 3.0
- * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0.
- */
-class JSONRenderer extends DefaultRenderer
-{
- public function contentType()
- {
- return 'application/json';
- }
-
- public function extension()
- {
- return '.json';
- }
-
- public function render($response)
- {
- if (!isset($response['Content-Type'])) {
- $response['Content-Type'] = $this->contentType() . ';charset=utf-8';
- }
-
- if (isset($response->body)) {
- $response->body = json_encode($response->body);
- }
- }
-}
diff --git a/lib/classes/searchtypes/MyCoursesSearch.class.php b/lib/classes/searchtypes/MyCoursesSearch.php
index 64bee64..1bdb250 100644
--- a/lib/classes/searchtypes/MyCoursesSearch.class.php
+++ b/lib/classes/searchtypes/MyCoursesSearch.php
@@ -1,6 +1,6 @@
<?php
/**
- * MyCoursesSearch.class.php
+ * MyCoursesSearch.php
* Search only in own courses.
*
* This program is free software; you can redistribute it and/or
diff --git a/lib/classes/searchtypes/NewsRangesSearch.php b/lib/classes/searchtypes/NewsRangesSearch.php
index 458d6f0..6cb7a2f 100644
--- a/lib/classes/searchtypes/NewsRangesSearch.php
+++ b/lib/classes/searchtypes/NewsRangesSearch.php
@@ -56,7 +56,7 @@ class NewsRangesSearch extends SearchType
LEFT JOIN `semester_courses` AS sc ON s.`Seminar_id` = sc.`course_id`
LEFT JOIN `semester_data` USING (`semester_id`)
WHERE {$sem_inst}.`institut_id` IN (:institutes)
- AND `name` LIKE :input
+ AND s.`name` LIKE :input
GROUP BY s.`Seminar_id`
ORDER BY s.`start_time` DESC
) AS course_select";
diff --git a/lib/classes/searchtypes/PermissionSearch.class.php b/lib/classes/searchtypes/PermissionSearch.php
index cc4ce3e..3f86271 100644
--- a/lib/classes/searchtypes/PermissionSearch.class.php
+++ b/lib/classes/searchtypes/PermissionSearch.php
@@ -12,7 +12,7 @@
/**
* Class of type SearchType used for searches with QuickSearch
- * (lib/classes/QuickSearch.class.php). You can search for people with a given
+ * (lib/classes/QuickSearch.php). You can search for people with a given
* Stud.IP permission level, either globally or at an institute.
*
* @author Thomas Hackl
diff --git a/lib/classes/searchtypes/RangeSearch.class.php b/lib/classes/searchtypes/RangeSearch.php
index caefae0..caefae0 100644
--- a/lib/classes/searchtypes/RangeSearch.class.php
+++ b/lib/classes/searchtypes/RangeSearch.php
diff --git a/lib/classes/searchtypes/ResourceSearch.class.php b/lib/classes/searchtypes/ResourceSearch.php
index 24038a8..f664beb 100644
--- a/lib/classes/searchtypes/ResourceSearch.class.php
+++ b/lib/classes/searchtypes/ResourceSearch.php
@@ -1,7 +1,7 @@
<?php
/**
- * ResourceSearch.class.php - A search type for resources.
+ * ResourceSearch.php - A search type for resources.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
diff --git a/lib/classes/searchtypes/RoomSearch.class.php b/lib/classes/searchtypes/RoomSearch.php
index 002712e..877c2cb 100644
--- a/lib/classes/searchtypes/RoomSearch.class.php
+++ b/lib/classes/searchtypes/RoomSearch.php
@@ -1,7 +1,7 @@
<?php
/**
- * RoomSearch.class.php - A search type for rooms.
+ * RoomSearch.php - A search type for rooms.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
diff --git a/lib/classes/searchtypes/SQLSearch.class.php b/lib/classes/searchtypes/SQLSearch.php
index 86aff6d..de4efa3 100644
--- a/lib/classes/searchtypes/SQLSearch.class.php
+++ b/lib/classes/searchtypes/SQLSearch.php
@@ -1,7 +1,7 @@
<?php
# Lifter010: TODO
/**
- * SQLSearch.class.php - Class of type SearchType used for searches with QuickSearch
+ * SQLSearch.php - Class of type SearchType used for searches with QuickSearch
*
* Long description for file (if any)...
*
@@ -17,7 +17,7 @@
/**
* Class of type SearchType used for searches with QuickSearch
- * (lib/classes/QuickSearch.class.php). You can search with a sql-syntax in the
+ * (lib/classes/QuickSearch.php). You can search with a sql-syntax in the
* database. You just need to give in a query like for a PDB-prepare statement
* and at least the variable ":input" in the query (the :input will be replaced
* with the input of the QuickSearch userinput.
diff --git a/lib/classes/searchtypes/SearchType.class.php b/lib/classes/searchtypes/SearchType.php
index d46d57e..b4f9b68 100644
--- a/lib/classes/searchtypes/SearchType.class.php
+++ b/lib/classes/searchtypes/SearchType.php
@@ -1,7 +1,7 @@
<?php
# Lifter010: TODO
/**
- * SQLSearch.class.php - A class-structure for alle search-objects in Stud.IP.
+ * SQLSearch.php - A class-structure for alle search-objects in Stud.IP.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
@@ -104,4 +104,3 @@ abstract class SearchType
*/
abstract public function includePath();
}
-
diff --git a/lib/classes/searchtypes/SeminarSearch.class.php b/lib/classes/searchtypes/SeminarSearch.php
index e490228..fd7bf8a 100644
--- a/lib/classes/searchtypes/SeminarSearch.class.php
+++ b/lib/classes/searchtypes/SeminarSearch.php
@@ -1,7 +1,7 @@
<?php
# Lifter010: TODO
/**
- * SeminarSearch.class.php
+ * SeminarSearch.php
* class to adapt StudipSemSearch to Quicksearch
*
* This program is free software; you can redistribute it and/or
diff --git a/lib/classes/searchtypes/StandardSearch.class.php b/lib/classes/searchtypes/StandardSearch.php
index 837a849..a3f0f3b 100644
--- a/lib/classes/searchtypes/StandardSearch.class.php
+++ b/lib/classes/searchtypes/StandardSearch.php
@@ -1,7 +1,7 @@
<?php
# Lifter010: TODO
/**
- * StandardSearch.class.php - Class of type SearchType used for searches with QuickSearch
+ * StandardSearch.php - Class of type SearchType used for searches with QuickSearch
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
@@ -15,7 +15,7 @@
/**
* Class of type SearchType used for searches with QuickSearch
- * (lib/classes/QuickSearch.class.php). You can search with a sql-syntax in the
+ * (lib/classes/QuickSearch.php). You can search with a sql-syntax in the
* database. You just need to give in a query like for a PDB-prepare statement
* and at least the variable ":input" in the query (the :input will be replaced
* with the input of the QuickSearch userinput.
diff --git a/lib/classes/searchtypes/TreeSearch.class.php b/lib/classes/searchtypes/TreeSearch.php
index 2fecf60..73d4239 100644
--- a/lib/classes/searchtypes/TreeSearch.class.php
+++ b/lib/classes/searchtypes/TreeSearch.php
@@ -1,6 +1,6 @@
<?php
/**
- * TreeSearch.class.php - Class of type SearchType used for searches with QuickSearch
+ * TreeSearch.php - Class of type SearchType used for searches with QuickSearch
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
diff --git a/lib/classes/sidebar/AttributesArrayAccessTrait.php b/lib/classes/sidebar/AttributesArrayAccessTrait.php
index 7bea834..09fc030 100644
--- a/lib/classes/sidebar/AttributesArrayAccessTrait.php
+++ b/lib/classes/sidebar/AttributesArrayAccessTrait.php
@@ -3,41 +3,25 @@ trait AttributesArrayAccessTrait
{
public $attributes = [];
- /**
- * @todo Add bool return type when Stud.IP requires PHP8 minimal
- */
- #[ReturnTypeWillChange]
- public function offsetExists($offset)
+ public function offsetExists($offset): bool
{
return isset($this->attributes[$offset]);
}
/**
- * @param $offset
- * @return mixed
- *
- * @todo Add mixed return type when Stud.IP requires PHP8 minimal
+ * @param string $offset
*/
- #[ReturnTypeWillChange]
- public function offsetGet($offset)
+ public function offsetGet($offset): mixed
{
return $this->attributes[$offset];
}
- /**
- * @todo Add void return type when Stud.IP requires PHP8 minimal
- */
- #[ReturnTypeWillChange]
- public function offsetSet($offset, $value)
+ public function offsetSet($offset, $value): void
{
$this->attributes[$offset] = $value;
}
- /**
- * @todo Add void return type when Stud.IP requires PHP8 minimal
- */
- #[ReturnTypeWillChange]
- public function offsetUnset($offset)
+ public function offsetUnset($offset): void
{
unset($this->attributes[$offset]);
}
diff --git a/lib/classes/sidebar/ClipboardWidget.class.php b/lib/classes/sidebar/ClipboardWidget.php
index abc0d6c..abc0d6c 100644
--- a/lib/classes/sidebar/ClipboardWidget.class.php
+++ b/lib/classes/sidebar/ClipboardWidget.php
diff --git a/lib/classes/sidebar/InstituteSelectWidget.class.php b/lib/classes/sidebar/InstituteSelectWidget.php
index ec05311..ec05311 100644
--- a/lib/classes/sidebar/InstituteSelectWidget.class.php
+++ b/lib/classes/sidebar/InstituteSelectWidget.php
diff --git a/lib/classes/sidebar/LinksWidget.php b/lib/classes/sidebar/LinksWidget.php
index bc8256d..72a2fca 100644
--- a/lib/classes/sidebar/LinksWidget.php
+++ b/lib/classes/sidebar/LinksWidget.php
@@ -30,7 +30,7 @@ class LinksWidget extends ListWidget
public function &addLink($label, $url, $icon = null, $attributes = [], $index = null)
{
if ($index === null) {
- $index = 'link-' . md5($url);
+ $index = 'link-' . md5($url . $label);
}
$element = new LinkElement($label, $url, $icon, $attributes);
$this->addElement($element, $index);
diff --git a/lib/classes/sidebar/ResourceTreeWidget.class.php b/lib/classes/sidebar/ResourceTreeWidget.php
index 60d4b29..60d4b29 100644
--- a/lib/classes/sidebar/ResourceTreeWidget.class.php
+++ b/lib/classes/sidebar/ResourceTreeWidget.php
diff --git a/lib/classes/sidebar/RoomClipboardWidget.class.php b/lib/classes/sidebar/RoomClipboardWidget.php
index 3d90c36..3d90c36 100644
--- a/lib/classes/sidebar/RoomClipboardWidget.class.php
+++ b/lib/classes/sidebar/RoomClipboardWidget.php
diff --git a/lib/classes/sidebar/RoomSearchTreeWidget.class.php b/lib/classes/sidebar/RoomSearchTreeWidget.php
index c3aaa07..c3aaa07 100644
--- a/lib/classes/sidebar/RoomSearchTreeWidget.class.php
+++ b/lib/classes/sidebar/RoomSearchTreeWidget.php
diff --git a/lib/classes/sidebar/RoomSearchWidget.class.php b/lib/classes/sidebar/RoomSearchWidget.php
index 1fa465f..1fa465f 100644
--- a/lib/classes/sidebar/RoomSearchWidget.class.php
+++ b/lib/classes/sidebar/RoomSearchWidget.php
diff --git a/lib/classes/sidebar/Sidebar.php b/lib/classes/sidebar/Sidebar.php
index 65fc62d..ba1e609 100644
--- a/lib/classes/sidebar/Sidebar.php
+++ b/lib/classes/sidebar/Sidebar.php
@@ -186,7 +186,7 @@ class Sidebar extends WidgetContainer
static $actions_widget_added = false;
- if ($widget instanceof NavigationWidget && !$navigation_widget_added) {
+ if ($widget instanceof NavigationWidget && !$navigation_widget_added && $widget->hasElements()) {
SkipLinks::addIndex(
_('Dritte Navigationsebene'),
$widget->getId(),
@@ -197,7 +197,7 @@ class Sidebar extends WidgetContainer
$navigation_widget_added = true;
}
- if ($widget instanceof ActionsWidget && !$actions_widget_added) {
+ if ($widget instanceof ActionsWidget && !$actions_widget_added && $widget->hasElements()) {
if (!$widget->getId()) {
$widget->setId('sidebar-actions');
}
diff --git a/lib/classes/sidebar/TemplateWidget.php b/lib/classes/sidebar/TemplateWidget.php
index c937c3b..2ca2d70 100644
--- a/lib/classes/sidebar/TemplateWidget.php
+++ b/lib/classes/sidebar/TemplateWidget.php
@@ -15,10 +15,10 @@ class TemplateWidget extends SidebarWidget
* Constructor of the widget.
*
* @param String $title Title of the widget
- * @param Flexi_Template $template Template for the widget
+ * @param Flexi\Template $template Template for the widget
* @param array $variables Associated variables for the template
*/
- public function __construct($title, Flexi_Template $template, array $variables = [])
+ public function __construct($title, Flexi\Template $template, array $variables = [])
{
parent::__construct();