diff options
Diffstat (limited to 'lib/classes')
| -rw-r--r-- | lib/classes/ActionMenu.php | 2 | ||||
| -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.php | 32 | ||||
| -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.php | 124 | ||||
| -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.php | 18 | ||||
| -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.php | 12 | ||||
| -rw-r--r-- | lib/classes/Debug/TraceableStudipPDO.php | 24 | ||||
| -rw-r--r-- | lib/classes/Debug/TrailsCollector.php | 69 | ||||
| -rw-r--r-- | lib/classes/Event.php (renamed from lib/classes/Event.interface.php) | 0 | ||||
| -rw-r--r-- | lib/classes/EventLog.php | 2 | ||||
| -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.php | 2 | ||||
| -rw-r--r-- | lib/classes/ForumEntry.php | 4 | ||||
| -rw-r--r-- | lib/classes/ForumHelpers.php | 10 | ||||
| -rw-r--r-- | lib/classes/Fullcalendar.php (renamed from lib/classes/Fullcalendar.class.php) | 2 | ||||
| -rw-r--r-- | lib/classes/I18N.php | 4 | ||||
| -rw-r--r-- | lib/classes/I18NString.php | 9 | ||||
| -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.php | 6 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Middlewares/Auth/OAuth1Strategy.php | 114 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Middlewares/Authentication.php | 44 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Middlewares/StudipMockNavigation.php | 27 | ||||
| -rw-r--r-- | lib/classes/JsonApi/NonJsonApiController.php | 4 | ||||
| -rw-r--r-- | lib/classes/JsonApi/RouteMap.php | 27 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Clipboards/Authority.php | 28 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsCreate.php | 106 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsDelete.php | 54 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsShow.php | 28 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Clipboards/ClipboardsCreate.php | 46 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Clipboards/ClipboardsDelete.php | 31 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Clipboards/ClipboardsUpdate.php | 50 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Consultations/SlotCreationCount.php | 105 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php | 14 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Files/RangeFileRefsIndex.php | 2 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Files/SubfilerefsIndex.php | 11 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Files/SubfoldersIndex.php | 23 | ||||
| -rw-r--r-- | lib/classes/JsonApi/SchemaMap.php | 4 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Schemas/Clipboard.php | 81 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Schemas/ClipboardItem.php | 61 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Schemas/File.php | 2 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Schemas/Folder.php | 12 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Schemas/WikiPage.php | 2 | ||||
| -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.php | 16 | ||||
| -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.php | 2 | ||||
| -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.php | 4 | ||||
| -rw-r--r-- | lib/classes/MvvQuickSearch.php | 2 | ||||
| -rw-r--r-- | lib/classes/MyRealmModel.php | 69 | ||||
| -rw-r--r-- | lib/classes/NotificationCenter.php (renamed from lib/classes/NotificationCenter.class.php) | 2 | ||||
| -rw-r--r-- | lib/classes/OAuth1.php | 167 | ||||
| -rw-r--r-- | lib/classes/OAuth2/NegotiatesWithPsr7.php | 6 | ||||
| -rw-r--r-- | lib/classes/OpenGraph.php | 81 | ||||
| -rw-r--r-- | lib/classes/PageLayout.php | 51 | ||||
| -rw-r--r-- | lib/classes/PluginAdministration.php | 23 | ||||
| -rw-r--r-- | lib/classes/PluginController.php | 72 | ||||
| -rw-r--r-- | lib/classes/Privacy.php | 3 | ||||
| -rw-r--r-- | lib/classes/PrivacyObject.php (renamed from lib/classes/PrivacyObject.interface.php) | 0 | ||||
| -rw-r--r-- | lib/classes/ProfileModel.php | 213 | ||||
| -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.php | 117 | ||||
| -rw-r--r-- | lib/classes/SQLQuery.php | 43 | ||||
| -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.php | 782 | ||||
| -rw-r--r-- | lib/classes/SimpleORMap.php | 2483 | ||||
| -rw-r--r-- | lib/classes/SimpleORMapCollection.php | 258 | ||||
| -rw-r--r-- | lib/classes/Siteinfo.php | 20 | ||||
| -rw-r--r-- | lib/classes/StudipArrayObject.php (renamed from lib/classes/StudipArrayObject.class.php) | 70 | ||||
| -rw-r--r-- | lib/classes/StudipAutoloader.php | 2 | ||||
| -rw-r--r-- | lib/classes/StudipCache.class.php | 94 | ||||
| -rw-r--r-- | lib/classes/StudipCachedArray.php | 27 | ||||
| -rw-r--r-- | lib/classes/StudipController.php | 885 | ||||
| -rw-r--r-- | lib/classes/StudipControllerPropertiesTrait.php | 69 | ||||
| -rw-r--r-- | lib/classes/StudipCoreFormat.php | 5 | ||||
| -rw-r--r-- | lib/classes/StudipDbCache.class.php | 123 | ||||
| -rw-r--r-- | lib/classes/StudipDispatcher.php | 64 | ||||
| -rw-r--r-- | lib/classes/StudipFileloader.php | 6 | ||||
| -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.php | 84 | ||||
| -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.php | 5 | ||||
| -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.php | 55 | ||||
| -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.php | 99 | ||||
| -rw-r--r-- | lib/classes/StudipTreeNodeCachableTrait.php | 2 | ||||
| -rw-r--r-- | lib/classes/StudygroupAvatar.php (renamed from lib/classes/StudygroupAvatar.class.php) | 0 | ||||
| -rw-r--r-- | lib/classes/StudygroupModel.php | 4 | ||||
| -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.php | 8 | ||||
| -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.php | 30 | ||||
| -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.php | 2 | ||||
| -rw-r--r-- | lib/classes/WidgetHelper.php | 402 | ||||
| -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.php | 4 | ||||
| -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.php | 213 | ||||
| -rw-r--r-- | lib/classes/cache/DbCache.php | 143 | ||||
| -rw-r--r-- | lib/classes/cache/Exception.php | 27 | ||||
| -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.php | 28 | ||||
| -rw-r--r-- | lib/classes/cache/Item.php | 164 | ||||
| -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.php | 98 | ||||
| -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.php | 2 | ||||
| -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.php | 4 | ||||
| -rw-r--r-- | lib/classes/coursewizardsteps/AdvancedBasicDataWizardStep.php | 2 | ||||
| -rw-r--r-- | lib/classes/coursewizardsteps/BasicDataWizardStep.php | 11 | ||||
| -rw-r--r-- | lib/classes/coursewizardsteps/LVGroupsWizardStep.php | 35 | ||||
| -rw-r--r-- | lib/classes/coursewizardsteps/StudyAreasWizardStep.php | 4 | ||||
| -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.php | 29 | ||||
| -rw-r--r-- | lib/classes/forms/CaptchaInput.php | 38 | ||||
| -rw-r--r-- | lib/classes/forms/Form.php | 12 | ||||
| -rw-r--r-- | lib/classes/forms/Input.php | 11 | ||||
| -rw-r--r-- | lib/classes/forms/Part.php | 2 | ||||
| -rw-r--r-- | lib/classes/globalsearch/GlobalSearchCourses.php | 4 | ||||
| -rw-r--r-- | lib/classes/globalsearch/GlobalSearchCourseware.php | 2 | ||||
| -rw-r--r-- | lib/classes/globalsearch/GlobalSearchMessages.php | 6 | ||||
| -rw-r--r-- | lib/classes/globalsearch/GlobalSearchMyCourses.php | 4 | ||||
| -rw-r--r-- | lib/classes/globalsearch/GlobalSearchUsers.php | 2 | ||||
| -rw-r--r-- | lib/classes/helpbar/Helpbar.php | 5 | ||||
| -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.php | 212 | ||||
| -rw-r--r-- | lib/classes/restapi/Response.php | 164 | ||||
| -rw-r--r-- | lib/classes/restapi/RouteMap.php | 1060 | ||||
| -rw-r--r-- | lib/classes/restapi/Router.php | 665 | ||||
| -rw-r--r-- | lib/classes/restapi/RouterException.php | 31 | ||||
| -rw-r--r-- | lib/classes/restapi/RouterHalt.php | 19 | ||||
| -rw-r--r-- | lib/classes/restapi/UriTemplate.php | 115 | ||||
| -rw-r--r-- | lib/classes/restapi/UserPermissions.php | 144 | ||||
| -rw-r--r-- | lib/classes/restapi/consumer/Base.php | 226 | ||||
| -rw-r--r-- | lib/classes/restapi/consumer/HTTP.php | 50 | ||||
| -rw-r--r-- | lib/classes/restapi/consumer/OAuth.php | 231 | ||||
| -rw-r--r-- | lib/classes/restapi/consumer/Studip.php | 36 | ||||
| -rw-r--r-- | lib/classes/restapi/renderer/DebugRenderer.php | 57 | ||||
| -rw-r--r-- | lib/classes/restapi/renderer/DefaultRenderer.php | 74 | ||||
| -rw-r--r-- | lib/classes/restapi/renderer/JSONRenderer.php | 35 | ||||
| -rw-r--r-- | lib/classes/searchtypes/MyCoursesSearch.php (renamed from lib/classes/searchtypes/MyCoursesSearch.class.php) | 2 | ||||
| -rw-r--r-- | lib/classes/searchtypes/NewsRangesSearch.php | 2 | ||||
| -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.php | 26 | ||||
| -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.php | 2 | ||||
| -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.php | 4 | ||||
| -rw-r--r-- | lib/classes/sidebar/TemplateWidget.php | 4 |
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(); |
